{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "initial_id",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:39.580119Z",
     "start_time": "2025-01-21T14:09:39.573580Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
      "  from .autonotebook import tqdm as notebook_tqdm\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=10, micro=14, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cu124\n",
      "cuda:0\n"
     ]
    }
   ],
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "\n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "1dad802b6500ab83",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.656263Z",
     "start_time": "2025-01-21T14:09:39.699901Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[(PosixPath('competitions/cifar-10/train/1.png'), 'frog'),\n",
      " (PosixPath('competitions/cifar-10/train/2.png'), 'truck'),\n",
      " (PosixPath('competitions/cifar-10/train/3.png'), 'truck'),\n",
      " (PosixPath('competitions/cifar-10/train/4.png'), 'deer'),\n",
      " (PosixPath('competitions/cifar-10/train/5.png'), 'automobile')]\n",
      "[(PosixPath('competitions/cifar-10/test/1.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/2.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/3.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/4.png'), 'cat'),\n",
      " (PosixPath('competitions/cifar-10/test/5.png'), 'cat')]\n",
      "50000 300000\n"
     ]
    }
   ],
   "source": [
    "from pathlib import Path\n",
    "\n",
    "# 目的：通过读取csv文件，得到图片的类别，并将图片路径和类别保存到DataFrame中。\n",
    "\n",
    "DATA_DIR1 = Path(\"./\")\n",
    "DATA_DIR2=Path(\"./competitions/cifar-10/\")\n",
    "\n",
    "train_labels_file = DATA_DIR1 / \"trainLabels.csv\"\n",
    "test_csv_file = DATA_DIR1 / \"sampleSubmission.csv\"  # 测试集模板csv文件\n",
    "train_folder = DATA_DIR2 / \"train/\"\n",
    "test_folder = DATA_DIR2 / \"test/\"\n",
    "\n",
    "# 所有的类别\n",
    "class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']\n",
    "\n",
    "\n",
    "# filepath:csv文件路径，folder:图片所在文件夹\n",
    "def parse_csv_file(filepath, folder):\n",
    "    result = []\n",
    "    # 读取所有行\n",
    "    # with 语句：用于管理文件的上下文。在 with 块结束时，文件会自动关闭，无需手动调用 f.close()。\n",
    "    with open(filepath, 'r') as f:\n",
    "        # f.readlines()：读取文件的所有行，返回一个列表，每个元素是一行内容。\n",
    "        # 第一行不需要，因为第一行是标题\n",
    "        lines = f.readlines()[1:]\n",
    "    for line in lines:\n",
    "        # strip('\\n')：去除行末的换行符（\\n）。\n",
    "        # split(',')：按','分割字符串，返回一个列表。\n",
    "        image_id, label_str = line.strip('\\n').split(',')\n",
    "        # 得到图片的路径\n",
    "        image_full_path = folder / f\"{image_id}.png\"\n",
    "        # 得到对应图片的路径和分类\n",
    "        result.append((image_full_path, label_str))\n",
    "    return result\n",
    "\n",
    "\n",
    "# 得到训练集和测试集的图片路径和分类\n",
    "train_labels_info = parse_csv_file(train_labels_file, train_folder)\n",
    "test_csv_info = parse_csv_file(test_csv_file, test_folder)\n",
    "\n",
    "#打印\n",
    "import pprint\n",
    "\n",
    "pprint.pprint(train_labels_info[0:5])\n",
    "pprint.pprint(test_csv_info[0:5])\n",
    "print(len(train_labels_info), len(test_csv_info))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8cd5063c39c678cf",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.732204Z",
     "start_time": "2025-01-21T14:09:41.658275Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                            filepath       class\n",
      "0  competitions/cifar-10/train/1.png        frog\n",
      "1  competitions/cifar-10/train/2.png       truck\n",
      "2  competitions/cifar-10/train/3.png       truck\n",
      "3  competitions/cifar-10/train/4.png        deer\n",
      "4  competitions/cifar-10/train/5.png  automobile\n",
      "                                filepath       class\n",
      "0  competitions/cifar-10/train/45001.png       horse\n",
      "1  competitions/cifar-10/train/45002.png  automobile\n",
      "2  competitions/cifar-10/train/45003.png        deer\n",
      "3  competitions/cifar-10/train/45004.png  automobile\n",
      "4  competitions/cifar-10/train/45005.png    airplane\n",
      "                           filepath class\n",
      "0  competitions/cifar-10/test/1.png   cat\n",
      "1  competitions/cifar-10/test/2.png   cat\n",
      "2  competitions/cifar-10/test/3.png   cat\n",
      "3  competitions/cifar-10/test/4.png   cat\n",
      "4  competitions/cifar-10/test/5.png   cat\n"
     ]
    }
   ],
   "source": [
    "# 取前45000张图片作为训练集\n",
    "train_df = pd.DataFrame(train_labels_info[0:45000])\n",
    "# 取后5000张图片作为验证集\n",
    "valid_df = pd.DataFrame(train_labels_info[45000:])\n",
    "# 测试集\n",
    "test_df = pd.DataFrame(test_csv_info)\n",
    "\n",
    "# 为 Pandas DataFrame 的列重新命名\n",
    "train_df.columns = ['filepath', 'class']\n",
    "valid_df.columns = ['filepath', 'class']\n",
    "test_df.columns = ['filepath', 'class']\n",
    "\n",
    "print(train_df.head())\n",
    "print(valid_df.head())\n",
    "print(test_df.head())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "af9444393c2debca",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.740688Z",
     "start_time": "2025-01-21T14:09:41.732204Z"
    }
   },
   "outputs": [],
   "source": [
    "from PIL import Image\n",
    "from torch.utils.data import Dataset, DataLoader\n",
    "from torchvision import transforms\n",
    "\n",
    "\n",
    "class Cifar10Dataset(Dataset):\n",
    "    df_map = {\n",
    "        \"train\": train_df,\n",
    "        \"eval\": valid_df,\n",
    "        \"test\": test_df\n",
    "    }\n",
    "    # enumerate(class_names)：遍历 class_names，返回索引和类别名称的元组\n",
    "    # 将类别名称映射为索引，通常用于将标签转换为模型可以处理的数值。\n",
    "    label_to_idx = {\n",
    "        label: idx for idx, label in enumerate(class_names)\n",
    "    }\n",
    "    # 将索引映射为类别名称，通常用于将模型的输出（索引）转换回可读的类别名称\n",
    "    idx_to_label = {\n",
    "        idx: label for idx, label in enumerate(class_names)\n",
    "    }\n",
    "\n",
    "    def __init__(self, mode, transform=None):\n",
    "        # 获取对应模式的df，不同字符串对应不同模式\n",
    "        self.df = self.df_map.get(mode, None)\n",
    "        if self.df is None:\n",
    "            raise ValueError(f\"Invalid mode: {mode}\")\n",
    "        self.transform = transform\n",
    "\n",
    "    def __getitem__(self, index):\n",
    "        # 获取图片路径和标签\n",
    "        img_path, label = self.df.iloc[index]\n",
    "        # 打开一张图片并将其转换为 RGB 格式\n",
    "        img = Image.open(img_path).convert('RGB')  # 确保图片具有三个通道\n",
    "        # 应用数据增强\n",
    "        img = self.transform(img)\n",
    "        # label 转换为 idx\n",
    "        label = self.label_to_idx[label]\n",
    "        return img, label\n",
    "\n",
    "    def __len__(self):\n",
    "        # 返回df的行数,样本数\n",
    "        return self.df.shape[0]\n",
    "\n",
    "\n",
    "IMAGE_SIZE = 32\n",
    "mean, std = [0.4914, 0.4822, 0.4465], [0.247, 0.243, 0.261]\n",
    "\n",
    "#数据增强\n",
    "# ToTensor还将图像的维度从[height, width, channels]转换为[channels, height, width]。\n",
    "transforms_train = transforms.Compose([\n",
    "    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),  # 缩放\n",
    "    transforms.RandomRotation(40),  # 随机旋转\n",
    "    transforms.RandomHorizontalFlip(),  # 随机水平翻转\n",
    "    transforms.ToTensor(),  # 转换为Tensor\n",
    "    transforms.Normalize(mean, std)  # 标准化\n",
    "])\n",
    "\n",
    "transforms_eval = transforms.Compose([\n",
    "    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize(mean, std)\n",
    "])\n",
    "\n",
    "train_ds = Cifar10Dataset(mode=\"train\", transform=transforms_train)\n",
    "eval_ds = Cifar10Dataset(mode=\"eval\", transform=transforms_eval)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "d17ef4a56f0b6ae1",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.748174Z",
     "start_time": "2025-01-21T14:09:41.742701Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([3, 32, 32])"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "train_ds[0][0].shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "161282a4e612afcd",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.753134Z",
     "start_time": "2025-01-21T14:09:41.749187Z"
    }
   },
   "outputs": [],
   "source": [
    "batch_size = 64\n",
    "\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size,\n",
    "                          shuffle=True)\n",
    "eval_loader = DataLoader(eval_ds, batch_size=batch_size,\n",
    "                         shuffle=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20b5b31416450980",
   "metadata": {},
   "source": [
    "## 模型定义"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "b73daeeb83b4b527",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.760182Z",
     "start_time": "2025-01-21T14:09:41.754138Z"
    }
   },
   "outputs": [],
   "source": [
    "class Resdiual(nn.Module):\n",
    "    \"\"\"\n",
    "    浅层的残差块，无bottleneck（性能限制\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, input_channels, output_channels,\n",
    "                 use_1x1conv=False, stride=1):\n",
    "        \"\"\"\n",
    "        残差块\n",
    "        params filters: 过滤器数目，决定输出通道\n",
    "        params use_1x1conv: 是否使用 1x1 卷积，此时 stride=2，进行降采样\n",
    "        params strides: 步长，默认为1，当降采样的时候设置为2\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        # [1+(n+2-3)]//1=n \n",
    "        # [1+(n+2-3)]//2=n//2\n",
    "        # 即该方法可能会降采样\n",
    "        self.conv1 = nn.Conv2d(\n",
    "            in_channels=input_channels,\n",
    "            out_channels=output_channels,\n",
    "            kernel_size=3,\n",
    "            stride=stride,\n",
    "            padding=1\n",
    "        )\n",
    "        # [1+(n+2-3)]//1=n \n",
    "        self.conv2 = nn.Conv2d(\n",
    "            in_channels=output_channels,\n",
    "            out_channels=output_channels,\n",
    "            kernel_size=3,\n",
    "            stride=1,\n",
    "            padding=1\n",
    "        )\n",
    "        if use_1x1conv:\n",
    "            # skip connection 的 1x1 卷积，用于改变通道数和降采样，使得最终可以做残差连接\n",
    "            # [1+n-1]//s=n//s\n",
    "            self.conv_sc = nn.Conv2d(\n",
    "                in_channels=input_channels,\n",
    "                out_channels=output_channels,\n",
    "                kernel_size=1,\n",
    "                stride=stride\n",
    "            )\n",
    "        else:\n",
    "            self.conv_sc = None\n",
    "\n",
    "        self.bn1 = nn.BatchNorm2d(output_channels, eps=1e-5, momentum=0.9)\n",
    "        self.bn2 = nn.BatchNorm2d(output_channels, eps=1e-5, momentum=0.9)\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        flow = F.relu(self.bn1(self.conv1(inputs)))  # 卷积->BN->ReLU\n",
    "        flow = self.bn2(self.conv2(flow))  # 卷积->BN\n",
    "        if self.conv_sc:\n",
    "            inputs = self.conv_sc(inputs)  # 1x1卷积，改变通道数和降采样\n",
    "        # 残差连接->ReLU，必须保证flow和inputs的shape相同\n",
    "        return F.relu(flow + inputs)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "fc16467b5391fd55",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.766476Z",
     "start_time": "2025-01-21T14:09:41.761185Z"
    }
   },
   "outputs": [],
   "source": [
    "class ResdiualBlock(nn.Module):\n",
    "    \"\"\"\n",
    "    若干个 Resdiual 模块堆叠在一起，\n",
    "    通常在第一个模块给 skip connection 使用 1x1conv with stride=2\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, input_channels, output_channels, num, is_first=False):\n",
    "        \"\"\"\n",
    "        params filters: 过滤器数目\n",
    "        params num: 堆叠几个 Resdiual 模块\n",
    "        params is_first: 是不是第一个block。 最上面一层 Resdiual 的 stride=1,is_first=False,图像尺寸减半，False图像尺寸不变\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        self.model = nn.Sequential()  # 用于存放 Resdiual 模块\n",
    "        # append() 等价于 add_module()\n",
    "        self.model.append(Resdiual(\n",
    "            input_channels=input_channels,\n",
    "            output_channels=output_channels,\n",
    "            use_1x1conv=not is_first,\n",
    "            stride=1 if is_first else 2\n",
    "        ))  # 第一个 Resdiual 模块，负责通道翻倍,图像的尺寸减半\n",
    "        for _ in range(1, num):\n",
    "            # 堆叠 num 个 Resdiual 模块\n",
    "            self.model.append(Resdiual(\n",
    "                input_channels=output_channels,\n",
    "                output_channels=output_channels,\n",
    "                use_1x1conv=False,\n",
    "                stride=1\n",
    "            ))\n",
    "\n",
    "    def forward(self, inputs):\n",
    "        return self.model(inputs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "8128761d5d6c0e1d",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.773688Z",
     "start_time": "2025-01-21T14:09:41.767479Z"
    }
   },
   "outputs": [],
   "source": [
    "class ResNetCifar10(nn.Module):\n",
    "    def __init__(self, n=3, num_classes=10):\n",
    "        \"\"\"\n",
    "        params units: 预测类别的数目\n",
    "        \"\"\"\n",
    "        super().__init__()\n",
    "        self.model = nn.Sequential(\n",
    "            # conv1\n",
    "            nn.Conv2d(in_channels=3, out_channels=16,\n",
    "                      kernel_size=3, stride=1),\n",
    "            nn.BatchNorm2d(16, momentum=0.9, eps=1e-5),\n",
    "            nn.ReLU(),\n",
    "            # conv2_x\n",
    "            ResdiualBlock(input_channels=16, output_channels=16,\n",
    "                          num=2 * n, is_first=True),\n",
    "            # conv3_x\n",
    "            ResdiualBlock(input_channels=16, output_channels=32,\n",
    "                          num=2 * n),\n",
    "            # conv4_x\n",
    "            ResdiualBlock(input_channels=32, output_channels=64,\n",
    "                          num=2 * n),\n",
    "            # 全局平均池化层\n",
    "            #无论输入图片大小，输出都是1x1，把width和height压缩为1\n",
    "            nn.AdaptiveAvgPool2d((1, 1)),\n",
    "            # 全连接层\n",
    "            nn.Flatten(),  # 64*1*1 -> 64\n",
    "            # 输出层\n",
    "            nn.Linear(in_features=64, out_features=num_classes)\n",
    "        )\n",
    "        self.init_weights()\n",
    "\n",
    "    def init_weights(self):\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, (nn.Linear, nn.Conv2d)):\n",
    "                # Kaiming 初始化（也称为 He 初始化）是为深度神经网络设计的一种初始化方法，特别适用于 ReLU 激活函数。\n",
    "                nn.init.kaiming_normal_(m.weight)\n",
    "                if m.bias is not None:\n",
    "                    nn.init.zeros_(m.bias)\n",
    "    \n",
    "    def forward(self, inputs):\n",
    "        return self.model(inputs)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "bb519b4756d62090",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.777720Z",
     "start_time": "2025-01-21T14:09:41.774691Z"
    }
   },
   "outputs": [],
   "source": [
    "# for key, value in ResNetCifar10(len(class_names)).named_parameters():\n",
    "#     print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "b294d25e7f6287d0",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.825513Z",
     "start_time": "2025-01-21T14:09:41.779726Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 1929546\n"
     ]
    }
   ],
   "source": [
    "total_params = sum(p.numel() for p in ResNetCifar10(len(class_names)).parameters() if p.requires_grad)\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "76ca49d0dca74376",
   "metadata": {},
   "source": [
    "## 模型训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "632d31ce33563b0c",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.909287Z",
     "start_time": "2025-01-21T14:09:41.826518Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "\n",
    "@torch.no_grad()  # 装饰器，禁止梯度计算\n",
    "def evaluate(model, data_loader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in data_loader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "\n",
    "        # 前向传播\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)  # 验证集损失\n",
    "        # tensor.item() 获取tensor的数值，loss是只有一个元素的tensor\n",
    "        loss_list.append(loss.item())\n",
    "\n",
    "        # 预测\n",
    "        preds = logits.argmax(axis=-1)  # 预测类别\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())  # tensor转numpy，再转list\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "\n",
    "    acc = accuracy_score(label_list, pred_list)  # 计算准确率\n",
    "    return np.mean(loss_list), acc  # # 返回验证集平均损失和准确率"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "28c2ba49f4ff546",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.915505Z",
     "start_time": "2025-01-21T14:09:41.910290Z"
    }
   },
   "outputs": [],
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=500, save_best_only=True):\n",
    "        self.save_dir = save_dir  # 保存路径\n",
    "        self.save_step = save_step  # 保存步数\n",
    "        self.save_best_only = save_best_only  # 是否只保存最好的模型\n",
    "        self.best_metric = -1  # 最好的指标，指标不可能为负数，所以初始化为-1\n",
    "        # 创建保存路径\n",
    "        if not os.path.exists(self.save_dir):  # 如果不存在保存路径，则创建\n",
    "            os.makedirs(self.save_dir)\n",
    "\n",
    "    # 对象被调用时：当你将对象像函数一样调用时，Python 会自动调用 __call__ 方法。\n",
    "    # state_dict() 返回模型参数的字典，包括模型参数和优化器参数\n",
    "    # metric 是指标，可以是验证集的准确率，也可以是其他指标\n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return  # 不是保存步数，则直接返回\n",
    "\n",
    "        if self.save_best_only:\n",
    "            assert metric is not None  # 必须传入metric\n",
    "            if metric >= self.best_metric:  # 如果当前指标大于最好的指标\n",
    "                # save checkpoint\n",
    "                # 保存最好的模型，覆盖之前的模型，不保存step，只保存state_dict，即模型参数，不保存优化器参数\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"08_resnet.ckpt\"))\n",
    "                self.best_metric = metric  # 更新最好的指标\n",
    "        else:\n",
    "            # 保存模型\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "            # 保存每个step的模型，不覆盖之前的模型，保存step，保存state_dict，即模型参数，不保存优化器参数\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "cd4d61586ad24eb5",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.921575Z",
     "start_time": "2025-01-21T14:09:41.916508Z"
    }
   },
   "outputs": [],
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        self.patience = patience  # 多少个step没有提升就停止训练\n",
    "        self.min_delta = min_delta  # 最小的提升幅度\n",
    "        self.best_metric = -1  # 记录的最好的指标\n",
    "        self.counter = 0  # 计数器，记录连续多少个step没有提升\n",
    "\n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:  # 如果指标提升了\n",
    "            self.best_metric = metric  # 更新最好的指标\n",
    "            self.counter = 0  # 计数器清零\n",
    "        else:\n",
    "            self.counter += 1  # 计数器加一\n",
    "\n",
    "    @property  # 使用@property装饰器，使得 对象.early_stop可以调用，不需要()\n",
    "    def early_stop(self):\n",
    "        # 如果计数器大于等于patience，则返回True，停止训练\n",
    "        return self.counter >= self.patience"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "725cf8a521379801",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.930997Z",
     "start_time": "2025-01-21T14:09:41.922578Z"
    }
   },
   "outputs": [],
   "source": [
    "def training(model,\n",
    "             train_loader,\n",
    "             val_loader,\n",
    "             epoch,\n",
    "             loss_fct,\n",
    "             optimizer,\n",
    "             save_ckpt_callback=None,\n",
    "             early_stop_callback=None,\n",
    "             eval_step=500,\n",
    "             ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "\n",
    "    global_step = 0  # 全局步数\n",
    "    model.train()  # 训练模式\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "\n",
    "                # 前向传播\n",
    "                logits = model(datas)\n",
    "                loss = loss_fct(logits, labels)  # 训练集损失\n",
    "                preds = logits.argmax(axis=-1)  # 预测类别\n",
    "\n",
    "                # 反向传播\n",
    "                optimizer.zero_grad()  # 梯度清零\n",
    "                loss.backward()  # 反向传播\n",
    "                optimizer.step()  # 优化器更新参数\n",
    "\n",
    "                # 计算准确率\n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())\n",
    "                loss = loss.cpu().item()\n",
    "\n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss,\n",
    "                    \"acc\": acc,\n",
    "                    \"step\": global_step\n",
    "                })\n",
    "\n",
    "                # 评估\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()  # 评估模式\n",
    "                    # 验证集损失和准确率\n",
    "                    val_loss, val_acc = evaluate(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss,\n",
    "                        \"acc\": val_acc,\n",
    "                        \"step\": global_step\n",
    "                    })\n",
    "                    model.train()  # 训练模式\n",
    "\n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        # model.state_dict() 返回模型参数的字典，包括模型参数和优化器参数\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), val_acc)\n",
    "                        # 保存最好的模型，覆盖之前的模型，保存step，保存state_dict,通过metric判断是否保存最好的模型\n",
    "\n",
    "                    # 3. 早停 early stopping\n",
    "                    if early_stop_callback is not None:\n",
    "                        # 验证集准确率不再提升，则停止训练\n",
    "                        early_stop_callback(val_acc)\n",
    "                        # 验证集准确率不再提升，则停止训练\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict  # 早停，返回记录字典 record_dict\n",
    "\n",
    "                # 更新进度条和全局步数\n",
    "                pbar.update(1)  # 更新进度条\n",
    "                global_step += 1  # 全局步数加一\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "\n",
    "    return record_dict  # 训练结束，返回记录字典 record_dict\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "22bf1e82c9715088",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:09:41.979444Z",
     "start_time": "2025-01-21T14:09:41.931999Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "epoch = 100\n",
    "\n",
    "model = ResNetCifar10(len(class_names))  # 定义模型\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "\n",
    "# 2. 定义优化器 采用 adam\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=0.001)\n",
    "\n",
    "# 3.save model checkpoint\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(save_dir=\"checkpoints\", save_step=len(train_loader), save_best_only=True)\n",
    "\n",
    "# 4. early stopping\n",
    "early_stop_callback = EarlyStopCallback(patience=5, min_delta=0.01)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "5335f62fe5e4977f",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.118493Z",
     "start_time": "2025-01-21T14:09:41.980446Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      " 28%|██▊       | 19712/70400 [23:51<1:01:22, 13.77it/s, epoch=27]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 28 / global_step 19712\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "model = model.to(device)  # 将模型移到GPU上\n",
    "\n",
    "# 训练过程\n",
    "record_dict = training(\n",
    "    model,\n",
    "    train_loader,\n",
    "    eval_loader,\n",
    "    epoch,\n",
    "    loss_fct,\n",
    "    optimizer,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "1a790e5f4635d5e8",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.120498Z",
     "start_time": "2025-01-21T14:10:19.119497Z"
    },
    "ExecutionIndicator": {
     "show": true
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAz8AAAHACAYAAACMDtamAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAmJRJREFUeJzs3Xl8VNX9//HXnTX7RiBhCasoIKsoCFjrwuJG1WrrgmutbVW0ll9rpV+Xaqu01lLUqlRba617rbYqFEEUF8ANBJFV9jWBQPZlMsv9/XFnJnvIJJNMQt7Px2OYyZ1775ycZMj9zOeczzFM0zQRERERERE5xtli3QAREREREZH2oOBHRERERES6BAU/IiIiIiLSJSj4ERERERGRLkHBj4iIiIiIdAkKfkREREREpEtQ8CMiIiIiIl2Cgh8REREREekSHLFuQEsEAgH2799PcnIyhmHEujkiIl2GaZqUlJTQq1cvbDZ9flaT/jaJiMRGJH+bOmXws3//fnJycmLdDBGRLmvPnj306dMn1s3oUPS3SUQktprzt6lTBj/JycmA9Q2mpKREfLzX62Xx4sVMnToVp9MZ7eZ1CerD6FA/tp76sH0VFxeTk5MT/n+4o/rwww/5wx/+wKpVqzhw4ABvvPEGF110UZPHLFu2jFmzZrF+/XpycnK46667uO6665r9mvrbFHvqw9ZTH0aH+rF9RfK3qVMGP6HhBCkpKS3+A5OQkEBKSop+IVtIfRgd6sfWUx/GRkcf1lVWVsaoUaP4wQ9+wHe/+92j7r9jxw7OP/98fvKTn/DCCy+wdOlSfvjDH9KzZ0+mTZvWrNfU36bYUx+2nvowOtSPsdGcv02dMvgRERFpyrnnnsu5557b7P3nz5/PgAED+OMf/wjA0KFD+fjjj/nTn/7U7OBHREQ6PgU/IiLS5a1cuZLJkyfX2jZt2jRuv/32Ro/xeDx4PJ7w18XFxYD1ia/X6424DaFjWnKsWNSHrac+jA71Y/uKpJ8V/IiISJeXm5tLVlZWrW1ZWVkUFxdTUVFBfHx8vWPmzJnDfffdV2/74sWLSUhIaHFblixZ0uJjxaI+bD31YXSoH9tHeXl5s/dV8CMibcLv97fLJ15erxeHw0FlZSV+v7/NX68rcDqd2O32WDejw5s9ezazZs0Kfx2acDt16tRG5/yYponf78fv92OaZq3nfD4fK1asYOLEiTgc+vPcEtHsQ8MwsNvt2O32Dj/HLZq8Xi9LlixhypQpmqvSCurH9hXKvDeH/ncVkagrLS1l79699S7u2oJpmmRnZ7Nnz54udYHSlgzDoE+fPiQlJcW6Ke0mOzubvLy8Wtvy8vJISUlpMOsD4Ha7cbvd9bY7nc4GL3aqqqo4cOBAo59Qhn6XDxw4oN/lFmqLPkxISKBnz564XK6onK+zaOz3WCKjfmwfkfSxgh8RiSq/38/evXtJSEige/fubX4RFwgEKC0tJSkpSYtuRoFpmhw6dIi9e/cyePDgLpMBmjBhAgsXLqy1bcmSJUyYMCEq5w8EAuzYsQO73U6vXr1wuVz13hv6XW69aPahaZpUVVVx6NAhduzYweDBg/VzETkGKPgRkajyer2Ypkn37t0b/cQ8mgKBAFVVVcTFxenCJEq6d+/Ozp078Xq9nTb4KS0tZevWreGvd+zYwZo1a8jIyKBv377Mnj2bffv28dxzzwHwk5/8hD//+c/ccccd/OAHP+C9997j1VdfZcGCBVFpT1VVFYFAgJycnEbnA+l3ufWi3Yfx8fE4nU527doVPq+IdG7631VE2oSG7XRex8LP7osvvmDMmDGMGTMGgFmzZjFmzBjuueceAA4cOMDu3bvD+w8YMIAFCxawZMkSRo0axR//+Ef++te/Rr3MtYKazkc/M5FjizI/IiJyzDnjjDOanHP27LPPNnjMl19+2YatEhGRWNPHGSIiIiIi0iUo+BERibL+/fszb968Vp3juuuu46KLLopKe0Q6imi8N0REWkPD3kREsIY8jR49OioXZp9//jmJiYmtb5RIB6D3hogcSxT8iIg0Q2hxyuYsnNi9e/d2aJFIx6D3hoh0Jl1y2Jv9H+dz1sY7oXhfrJsicswzTZPyKl+b3iqq/A1ub+4iq9dddx0ffPABjzzyCIZhYBgGzz77LIZh8L///Y+xY8fidrv5+OOP2bZtGxdeeCFZWVkkJSVxyimn8O6779Y6X92hPYZh8Ne//pWLL76YhIQEBg8ezJtvvhlRP3o8Hm677TZ69OhBXFwcp512Gp9//nn4+YKCAmbMmBEuMT548GD+/ve/A1aZ5ZkzZ9KzZ0/i4uLo168fc+bMiej1Jboae1809rsczVskiw93xPfG559/zpQpU8jMzCQ1NZVvf/vbrF69utY+hYWF/PjHPyYrK4u4uDiGDx/O22+/HX5++fLlnHHGGSQkJJCens60adMoKChodr+ISNM2Hijmqr9+yto9hbFuSj1dMvNjHN5CcmUB3qqyWDdF5JhX4fUz7J53YvLaG+6fRoLr6P/NPfLII2zZsoXhw4dz//33A7B+/XoA7rzzTh5++GEGDhxIeno6e/bs4bzzzuOBBx7A7Xbz3HPPMX36dDZv3kzfvn0bfY377ruPhx56iD/84Q889thjzJgxg127dpGRkQFYF4XXXXcdv/71rxs8/o477uDf//43//jHP+jXrx8PPfQQ06ZNY+vWrWRkZHD33XezYcMG/ve//5GZmcnWrVupqKgA4NFHH+XNN9/k1VdfpW/fvuzZs4c9e/ZE0pUSZZ3hfQEd871RUlLCtddey2OPPYZpmvzxj3/kvPPO45tvviExMZFAIMD5559PSUkJzz//PIMGDWLDhg3hNavWrFnD2WefzQ9+8AMeeeQRHA4H77//Pn6/v6VdKiJ1PPbeN3y8NZ8Kr59/3zQx1s2ppUsGPziCi5R5K2LbDhHpEFJTU3G5XCQkJJCdnQ3Apk2bALj//vuZMmVKeN+MjAxGjRoV/vo3v/kNb7zxBm+++SYzZ85s9DWuu+46rrjiCgAefPBBHn30UT777DPOOeccAAYNGkRmZmaDx5aVlfHkk0/y7LPPcu655wLw9NNPs2TJEv72t7/xi1/8gt27dzNmzBhOPvlkwLpgDNm9ezeDBw/mtNNOwzAM+vXrF2kXSRfVEd8bZ511Vq3jn3rqKdLS0vjggw8477zzWLZsGZ999hkbN27k+OOPB2DgwIHh/R966CFOPvlknnjiifC2E088MbKOEZFGVXr9vL/pEACrdhWwbm8RI/qkxrhV1bp08GP4PTFuiMixL95pZ8P90V0osqZAIEBJcQnJKcn1FiOMd9pbff5QMBFSWlrKr3/9axYsWMCBAwfw+XxUVFTUWjCzISNHjgw/TkxMJCUlhYMHD4a3LV26tNFjt23bhtfrZdKkSeFtTqeTcePGsXHjRgBuuukmLrnkElavXs3UqVO56KKLmDjR+rTtuuuuY8qUKZxwwgmcc845XHDBBUydOrX5nSBR19D7oqnf5Wi/djTE6r2Rl5fHXXfdxbJlyzh48CB+v5/y8vLw66xbt44+ffqEA5+61qxZw/e+972IvlcRab4PtxyiwludSX12xU7++P1RTRzRvrp08IO3MrbtEOkCDMNo9hCblggEAvhcdhJcjja5YKxbmernP/85S5Ys4eGHH+a4444jPj6eSy+9lKqqqibP43Q6a31tGAaBQCBq7Tz33HPZtWsXCxcuZMmSJZx99tnccsstPPzww5x00kns2LGD//3vf7z77rt8//vfZ/Lkybz22mtRe32JTEPvi7b+XY62WL03rr32Wg4fPswjjzxCv379cLvdTJgwIfw68fHxTb7e0Z4XkdZZtD4XgJP7pfPFrgLe+mo/vzpvCN2S3DFumaXj/+/aBkxHsPN9Cn5ExOJyuZo15n/58uVcd911XHzxxYwYMYLs7Gx27tzZpm0bNGgQLpeL5cuXh7d5vV4+//xzhg0bFt7WvXt3rr32Wp5//nnmzZvHU089FX4uJSWFyy67jKeffppXXnmFf//73xw5cqRN2y3Hho723li+fDm33XYb5513HieeeCJut5v8/Pzw8yeeeCJ79+5ly5YtDR4/cuTIJjOtItJyXn+AdzfkAfCLaScwsk8qVb4AL3/eceaZdsngJ5z5UfAjIkH9+/fn008/ZefOneTn5zf6yfPgwYN5/fXXWbNmDWvXruXKK6+MSgbn7LPP5s9//nODzyUmJnLTTTfxi1/8gkWLFrFhwwZuvPFGysvLueGGGwC45557+O9//8vWrVtZv349b7/9NkOHDgVg7ty5vPTSS2zatIktW7bwr3/9i+zsbNLS0lrdbjn2dbT3xuDBg/nnP//Jxo0b+fTTT5kxY0atbM6kSZM4/fTTueSSS1iyZEk467lo0SIAZs+ezeeff87NN9/MV199xaZNm3jyySdrBVAi0jKfbD9McaWPzCQXJ/fP4NoJ/QF4/pNdeP3RG+3QGgp+RESwhuzY7XaGDRtG9+7dG52nMHfuXNLT05k4cSLTp09n2rRpnHTSSa1+/W3btjV58fW73/2OSy65hKuvvpqTTjqJrVu38s4775Ceng5Yn87Pnj2bkSNHcvrpp2O323n55ZcBSE5ODk/yPuWUU9i5cycLFy7sFEOrJPY62nvjb3/7GwUFBZx00klcffXV4RLwNf3rX//ilFNO4YorrmDYsGHccccd4ezV8ccfz+LFi1m7di3jxo1jwoQJ/Pe//23WOkUi0rRFX1tD3qYMy8ZuM7hgVE8yk1wcKKpk8fq8GLfOYpiRFPzvIIqLi0lNTaWoqIiUlJSIjw+8cBm2bxbhO28ujnE3tEELj31er5eFCxdy3nnn1RuvLc13LPZjZWUlO3bsYMCAAcTFxbX56wUCAYqLi0lJSdHFfJQ09TNs7f+/x7Km+qY57wv9LrdeW/Rhe/+fFmvH4t+lWOiK/egPmIx/cCn5pR7+8YNxfPt4a1HjPy7ezGPvbWVc/wxe/cmENnntSP42dc3/XYNzfgyfqr2JiIiIiLTWl7sLyC/1kBznYMLAbuHtM8b3w2Ez+GznEdbvL4phCy1dNPjRsDcRERERkWgJDXmbPDQLl6M6xMhOjWPacGudsH+s2BmLptXSJYMfU8GPiIiIiEhUmKYZLnE97cTses9fP7E/AP9ds5+CsqbL37e1Lhn8VGd+NOxNRERERKQ11u8vZm9BBXFOW3iuT01j+6VzYq8UPB2g7HXXDH6coeCnIrbtEBERERHp5N4JZn3OOL4H8S57vecNw+DaYPbn+U924Yth2euuGfzYQ4ucKvMjIiIiItIaofk+5wyvP+Qt5DujepGR6GJfYQXvboxd2euuGfwEMz+G5vyIiIiIiLTY1oOlfHOwFKfd4MwhPRrdL85p5/JTcgB4NoaFD7pm8KOCByIiIiIirRYa8jZxUCap8U2vaXTVqf2w2ww+2X6ETbnF7dG8erpk8BOu9uZV8CMiIiIi0lKh4KepIW8hvdLimXZiFhC7stddMvgJZ378mvMjItHTv39/5s2b1+jzzz77LGlpae3WHpGO4GjvCxHpvPYVVvDV3iIMw1rfpzmundAfgDe+3EdhefuXve7awY9X1d5ERERERFrinWChg1P6ZdA92d2sY8YNyGBIdjKV3gCvftH+Za+7aPCjam8iIiIiIq0RXti0GUPeQgzD4Lpg2et/r9qHaZpt0bRGddHgJx5QtTeRdmGaUFXWtjdvecPbI/gP9amnnqJXr14EArXXHrjwwgv5wQ9+wLZt27jwwgvJysoiKSmJU045hXfffbfV3fPkk08yaNAgXC4XJ5xwAv/85z9rdJ3Jr3/9a/r27Yvb7aZXr17cdttt4eefeOIJBg8eTFxcHFlZWVx66aWtbo+0k8beF439Lkfz1sHeF4cPH+aKK66gd+/eJCQkMGLECF566aVa+wQCAR566CGOO+443G43ffv25YEHHgg/v3fvXq644goyMzPp3bs348aN49NPP42oHSISmUMlHj7feQQgPI+nuS4c3Zt7pw/j1Z9MwDCMtmheoxzt+modRTjzo+BHpM15y+HBXm12ehuQ1tiTv9oPrsRmned73/set956K++//z5nn302AEeOHGHRokUsXLiQ0tJSzjvvPB544AHcbjfPPfcc06dPZ/PmzfTt27fBc1533XXs3LmTZcuWNfj8G2+8wU9/+lPmzZvH5MmTefvtt7n++uvp06cPZ555Jv/+97/505/+xMsvv8yJJ55Ibm4ua9euBeCLL77gtttu45///CcTJ07kyJEjfPTRR836XqUDaOB90eTvcjR1sPdFZWUlY8eO5Ze//CUpKSksWLCAq6++mkGDBjFu3DgAZs+ezdNPP82f/vQnTjvtNA4cOMCmTZsAKC0t5dvf/ja9e/fmP//5D0lJSWzZsqVewCYi0fXuxjxME0b0TqVPekJEx8a77Fw/aUAbtaxpXTL4MVXqWkTqSE9P59xzz+XFF18MX+S99tprZGZmcuaZZ2Kz2Rg1alR4/9/85je88cYbvPnmm8ycObPBc/bs2bPJC7CHH36Y6667jptvvhmAWbNm8cknn/Dwww9z5plnsnv3brKzs5k8eTJOp5O+ffuGLwZ3795NYmIiF1xwAcnJyfTr148xY8ZEqztEgPZ5X/Tu3Zuf//zn4a9vvfVW3nnnHV599VXGjRtHSUkJjzzyCH/+85+59tprARg0aBCnnXYaAC+++CKHDh3i888/Jy0tjeLiYkaPHo3N1jUHt4i0l+YsbNoRdcngp3qdH835EWlzzgTrk+Y2EggEKC4pISU5uf7FjjOyT6JmzJjBjTfeyBNPPIHb7eaFF17g8ssvx2azUVpayq9//WsWLFjAgQMH8Pl8VFRUsHv37kbPN2fOnCZfb+PGjfzoRz+qtW3SpEk88sgjgPWp+7x58xg4cCDnnHMO5513HtOnT8fhcDBlyhT69esXfu6cc87h4osvJiEhsu9ZYqSB90WTv8vRfu0ItPX7wu/38+CDD/Lqq6+yb98+qqqq8Hg84d/ljRs34vF4wsFXXWvWrGHMmDFkZGQo2yPSToorvazYlg/AtBM7V/DTNT8WcSrzI9JuDMMaYtOWN2dCw9sjHEc8ffp0TNNkwYIF7Nmzh48++ogZM2YA8POf/5w33niDBx98kI8++og1a9YwYsQIqqrarkxnTk4Omzdv5oknniA+Pp6bb76Z008/Ha/XS3JyMqtXr+all16iZ8+e3HPPPYwaNYrCwsI2a49EUWPvi8Z+l6N562Dviz/84Q888sgj/PKXv+T9999nzZo1TJs2LXyO+Pj4Jo8/2vMi0Xa41MMbX+6l0uuPdVNi5v1NB/H6TY7rkcRxPZJi3ZyIdM3gx15jzk87V5gQkY4rLi6O7373u7zwwgu89NJLnHDCCZx00kkALF++nOuuu46LL76YESNGkJ2dzc6dO1v1ekOHDmX58uW1ti1fvpxhw4aFv46Pj2f69Ok8+uijLFu2jJUrV7Ju3ToAHA4HkydP5qGHHuKrr75i586dvPfee61qk0hdbf2+WL58ORdeeCFXXXUVo0aNYuDAgWzZsiX8/ODBg4mPj2fp0qUNHj9y5EjWrFnDkSNHWvw9ikTij0u28LNX1vKvGJRp7ije23QQiLzQQUfQNYe9OYPV3swA+L3gcMW4QSLSUcyYMYMLLriA9evXc9VVV4W3Dx48mNdff53p06djGAZ33333UYfYzJ49m3379vHcc881+PwvfvELvv/97zNmzBgmT57MW2+9xeuvvx6ulvXss8/i9/sZP348CQkJPP/888THx9OvXz/efvtttm/fzumnn056ejoLFy4kEAhwwgknRK8zRILa8n0xePBgXnvtNVasWEF6ejpz584lLy8v/CFAXFwcv/zlL7njjjtwuVxMmjSJQ4cOsX79em644QauuOIKHnzwQS666CIeeOABkpOT+eabb+jTpw8TJkxou06RLmvTgWIAvt5XHOOWxM6G/db3fnL/jBi3JHJdM/PjqLEIk4a+iUgNZ511FhkZGWzevJkrr7wyvH3u3Lmkp6czceJEpk+fzrRp08KffjfmwIEDTc59uOiii3jkkUd4+OGHOfHEE/nLX/7C3//+d8444wwA0tLSePrpp5k0aRIjR47k3Xff5a233qJbt26kpaXx+uuvc9ZZZzF06FDmz5/PSy+9xIknnhiVfhCpqS3fF3fddRcnnXQS06ZN44wzziA7O5uLLrqo1jF33303/+///T/uuecehg4dymWXXcbBg9Ynzy6Xi8WLF9OjRw8uuOACJk2axEMPPYTdbo9eB4jUsPNwOQBbD5XGuCWxUen1sz2/DIBhPVNi3JrIGWaEKwt9+OGH/OEPf2DVqlUcOHCAN954o9Z/UqZpcu+99/L0009TWFjIpEmTePLJJxk8eHB4nyNHjnDrrbfy1ltvYbPZuOSSS3jkkUdISmremMHi4mJSU1MpKioiJSXyTvdWVeF8sLv1xc+/gaQeEZ+jq/N6vSxcuJDzzjsPp9MZ6+Z0WsdiP1ZWVrJjxw4GDBhAXFxcm79eIBCguLiYlJQUVXeKkqZ+hq39//dY1lTfNOd9od/l1muLPmzv/9Ni7Vj8uxRNRRVeRt23GIDUeCdr7pnS4Do1x3I/rttbxPQ/f0x6gpPVdzf8/be3SP42Rfw/Q1lZGaNGjeLxxx9v8PmHHnqIRx99lPnz5/Ppp5+SmJjItGnTqKyszrDMmDGD9evXs2TJEt5++20+/PDDehWP2pRh4DeCv4jK/IiIiIhIM+w6XBZ+XFThJb+07YredFQbg8P+hvZM6RCBT6QinvNz7rnncu655zb4nGmazJs3j7vuuosLL7wQgOeee46srCz+85//cPnll7Nx40YWLVrE559/zsknnwzAY489xnnnncfDDz9Mr15ttxhiTX6bC7vfC14FPyIiIiJydKEhbyFbD5bSPdndyN6xY5ompgk2W/SDkw01gp/OKKoFD3bs2EFubi6TJ08Ob0tNTWX8+PGsXLmSyy+/nJUrV5KWlhYOfAAmT56MzWbj008/5eKLL653Xo/Hg8dTvSZPcbHV6V6vF6/XG3E7vV4vtmDmx1tZCi04R1cX6veW9L9UOxb70ev1YpomgUCgXdbcCI3cDb2mtF4gEMA0Tbxeb715E8fS76qISKR25pfV+nrroVImDOoWo9Y0LBAwufDx5VR6/bx922m4HdGd/7ZRwU+13FxrpdesrNpl77KyssLP5ebm0qNH7Tk2DoeDjIyM8D51zZkzh/vuu6/e9sWLF7d4Qb/JNiv4WfnR+xQkdt1Sha21ZMmSWDfhmHAs9aPD4SA7O5vS0tI2XQOnrpKSknZ7rWNdVVUVFRUVfPjhh/h8vlrPlZeXN3KUiMixb2dw2JvLbqPKH2DbwY5X9GD3kXLW7SsC4MvdhZw6MHrBmWmaNYKf5Kidtz11ilLXs2fPZtasWeGvi4uLycnJYerUqS0reOD14ttolbeeeMoYzP7filpbuwqv18uSJUuYMmXKMTeRrz0di/3o8XjYvXs3iYmJ7bL4oGmalJSUkJyc3CnHHndEFRUVxMfH8+1vfxu3u/ZwjlDmXVomwhpD0gHoZyY17QoOe5swqBsfbDnE1g4Y/ISCE4AV2w5HNfg5UFRJcaUPh83odIubhkQ1+MnOzgYgLy+Pnj17hrfn5eUxevTo8D6h8pQhPp+PI0eOhI+vy+121/sDDOB0Olt8wVgVHPbmwA/HyEVnLLTmZyDVjrV+NAwDn8/XLhWrQkPdDMNQhawo8fl8GIaB2+2u93t5LP2etqdQv5WXl7fLhwISPaFsp373BaqHvU0eltUpgp9Pth2GKdE/93E9kqI+nK69RDX4GTBgANnZ2SxdujQc7BQXF/Ppp59y0003ATBhwgQKCwtZtWoVY8eOBeC9994jEAgwfvz4aDanSf7gsDe8Fe32miJdgcPhICEhgUOHDuF0Ots8IAkEAlRVVVFZWangJwoCgQCHDh0iISEBh6NTDA7oFOx2O2lpaeEP/xISEuplKvW73HrR7EPTNCkvL+fgwYOkpaVp3SChuNLL4TJrOPfZQ3pwN5BbXElJpZfkuI4THG84UD0M/Ms9BVRU+Yl3Ref3NxT8DMnunEPeoAXBT2lpKVu3bg1/vWPHDtasWUNGRgZ9+/bl9ttv57e//S2DBw9mwIAB3H333fTq1Su8FtDQoUM555xzuPHGG5k/fz5er5eZM2dy+eWXt1ulN4BAuNS1p+kdRSQihmHQs2dPduzYwa5du9r89UzTDA/T0rC36LDZbPTt21f9GWWh0Q11Rz+E6He59dqiD9PS0hodmSJdy658KwuYmeSmV1o83ZPdHCrxsO1QGaNz0mLbuBpCAYphgNdv8sWuI3xrcPcondsKrDprsQNoQfDzxRdfcOaZZ4a/Ds3Fufbaa3n22We54447KCsr40c/+hGFhYWcdtppLFq0qNbCYC+88AIzZ87k7LPPDi9y+uijj0bh22k+v82a84NPmR+RaHO5XAwePLhdCh54vV4+/PBDTj/9dA1LiRKXy6XMQxsIfTDQo0ePBqvm6Xe59aLdh06nUxkfCQsVO+jfzSq2dVz3JA6VeNh6sLTDBD9FFV72FVrXtmcPyeLdjXms2HY4isFP5670Bi0Ifs4444wmJ/8ZhsH999/P/fff3+g+GRkZvPjii5G+dFSFh70p8yPSJmw2W7ushm632/H5fMTFxemCUToFu93e4AW1fpdbT30obSk036d/ZiJgzXtZuf1wh5r3sykYnPRKjePc4dnh4Ccayqt87AgGgJ05+OmyH+0FjGDmR3N+REREROQoQguchjM/wWpnHSr4ya0elhZaf2jd3kKKK1u/Rtvm3BJM0xr21xEXdm2uLhv8KPMjIiIiIs2163D9zA/AtkMdJ/ipOSytV1o8/bslEDDh8x1HWn3u6sCq8xY7gC4d/GjOj4iIiIg0T/Wcn9rBz67DZXh8/pi1q6a6c3JC2Z+VURj6dizM94EuHPyo2puIiIiINEdJpZf8UquIT7/gsLceyW6S3Q4CJuwMVoKLJX/AZHNe7ezMhEGZAFGZ91Md/Cjz0ylpnR8RERGRjmfpxrzwELOOYtfhUJlrV3hNH8MwGNQG834CAZN31uey+3BkAdWO/DIqvQHinDb6BbNTpw7MAGBjbjEFZS2vwGqaJpuOgTLX0IWDH2V+RERERDqWdXuLuOEfX3DLi6tj3ZRaQkPeQkFFSLSLHhSVe7nhH5/z43+u4uYXV0V0bCgzc0J2Cnabtc5Vj+Q4BvdIwjTh0x0tz/7sLaigxOPDZbcxqHtSi8/TEXTZ4EdzfkREREQ6lm8OWtmF9fuLKYlChbJoCZe5biz4iULRg6/3FXHBnz/i/c2Hgl8Xk1/a/A/pQ8HPsDrD0iYG5/20ZujbhuC5j+uRhNPeucOHiNf5OVao2puIiIhIx3KgqBIA04Sv9hYx6bjMGLfIUrfMdchx3aOT+Xlt1V7+7411eHwBcjLiMU0r2/LJ9sNc0KsMCnYBJpiBGrfaXydv28p3bEVMtQ2Eom6Q2huwih78Y+WuVhU9OFaKHUAXDn60zo+IiIhIx7KvsPq6bM2ewo4T/NRZ4DQklPnZfqgUf8AMDzdrLo/Pz/1vbeCFT3cDcOYJ3Zl32RjmLd3CwuWr6fX+7VDwTrPOdROAC1gDrJkFqX2h76l8K/sUTrAF2HKwNwdLKumRHPkC5NXzfZoodlBRAHtXwd7PYe9nkLcBegyBodNhyAWQnB3x67aFLhv8KPMjIiIi0rHsrxP8dBTVmZ/awU9ORgIuhw2PL8C+ggr61skMNWV/YQU3vbCatXsKMQy4/ezjufWs47D5K5nh+Re/cD9BQkHwOjVrONgcYNjAMIL31TdfwOSznYUEMJjQy4794Hoo2g3rdpO47lXecUGRmUDF86fA8DOg7wToNQaczQuENubWyfwE/HBwQzDQ+QL2fAaHv6l/YGkubF8GC34OfU6xAqGhF0DGwGb3U7R14eBHc35EREREOpK6wY9pmhhGZNmUaCv1+MJzb/pl1g5u7DaDgZmJbMotYeuhkmYHP8u35nPrS19ypKyK1Hgn8y4fzZnHd4cN/4HF93Bc0W4w4IvA8fSf8QiZJ0xs8nyfbcvnyqc/JScjno9uOgs8pbDvC9i1EnavpGrXZ6QGyknN+wDyPgg23mUFQMk9rcd2p3WzOYNfO8DuwmPaOb9wB1V2Byd98yl8vBr2rQZvAxX5MgZZQU7OKdBjmBUcbXyrOhu09zNYcjdkjbCCoKHTrf3a8WfcZYMfVXsTERER6Vj2F1aGHx8q8XCgqJJeafExbFH1kLduiS5SgmWuaxrUI8kKfg6WctaQrKOeb9WuI1z9t08JmHBirxTmXzWWHM838Ox1sGu5tVNKbx42r+TPh0Yzt6wv3z3KOTcGh6UNyQ5mZtxJMPAM6wYs37CXuf98g6nJ27l1UD7s/gTKDsKeT4/aXjdwR+jbrrm7Kxn6jLWCnT6nQO+TIbFb7YP7TYRJP4Xi/bBpgRUI7fwY8tZZt2VzrCzQ0OkwZDr0Hgu2ti2o0GWDH63zIyIiItJxFFd6KfX4ABjYPZHth8pYs6cwdsFP6UGwOdiVbwVkdef7hERa9OCVz/cQMOGsIT144sI+xH14B3z5PGCCIx5Oux0m3oZ36S74YDsrtx3muyf1afKcRytIcPLALDYYg1hXPJDvTjuL3qlxcGS7NWStshD8XvBXQcBn3fu94cdbDhSwZsdB+qQ6mHhCH+h9EvQZB91PAJu9Wd8zKb1g3I3WrfwIbP4fbHobti612rH8Eet27dsw4FvNO2cLddngJ6A5PyIiIiIdRmjIW0aii1MHdgsHP+eN6Nm+DfGUwv9+CWueB+Acw8nH7lR8xT3glUGQlG1N3k/OhqRsxrjtZFDM9ryio57a5w+wZEMeLrzck7GEuCefgCora8PwS2HKfZBqBToTBnbjLx9sb1aJ6sbKXIckxzkZ0TuVNXsKWbntMJeO7QPdBlm3o3j2jXW8+M1ubhoxiInnDDnq/keVkAFjZlg3TylsXRIcGveFNRepjXXZ4Mcfqvbmq2x6RxERERFpc6Hgp2dqHKNz0njx093tX/TgwFfw2g9qTd63m176GPlQkQ8bN9Q75AxgdRxwCMz77BiOOBwOF9O8Jo4dd4PDDXY3OFyUee084q1gYFwefVYftE7Qawyc83voO77WeU/pn4HDZrCvsII9R8rJyWh4PpHPH+CbPCvr1FQp6gmDutUOfpopFFgNyW6i0ltLuZPgxIutWyDQ5kPeoCsHP+HMj4IfERERkVjbF5zv0ystntE5aQCs21uEzx/A0dYLa5omfPYULL7LGvaV3BO++zTkjOOmvywkd+8ufvWtNE7J9EJJrnUrte7NklyM8nwADNMP3jIMbxlxAIWFtV4mFTg9NFIsKRsm3wsjL2/woj/R7WBUThqrdhWwYls+l2X0bbDp2/PLqPIHSHTZyUlvvODCxEHdeHLZNlZuy292IYlAwGRzrpWZGtbWa/y0Q+ADXTj4qS54oOBHREREJNZCmZ/eafEM6p5EkttBqcfHlrxShvVq4MK7qgwObYaMARCf3vIXLj8C/70FNi+0vj7+XLjw8fDk/S8KkzlkDsY9chL0Sat3uAGc/dBiDh85wtMzRnBKn0S8lWV8vGwp35owDgc+8HkIeD3c9e9VlJeX88NvH8fwM74P7qazKRMHdWPVrgJWbjvMZac0HPyEMzM9U7A1sc7Qyf0ycNoN9hdVsutweaNzmGrafaSc8io/LoeNAc3YvzPossFPuNR1wAd+n1XOT0RERERi4kAw+OmVFofdZjCyTyorth1m7d5Cay7Lke3BksmfW+vK5K0H028NKRt2IZx0NfQ7LbIMws6P4d83Qsl+q7zzlN/A+B+HSy+XenwcKgmWue7W+MV//x7pbDviZVNpIqek9wevl+KErZi9x4LT+sB97e4CXiw1SXI7+N1Zk8F59GIBEwZ147H3trJi2+FGszUbwsUOmg6k4l12xuSk89nOI6zcfrhZwU8osDohK7nts2/tpMte8YcLHoCV/bEnxa4xIiIiIl3c/hrD3qgs5jsp33CS/QNGf/QYvL8ZKo7UPyguFSqLYN2r1i19gBUEjZ5hFSRojN8HH/4BPnwIzAB0Ow4ufQZ6jqq1267DVpnrjEQXqfH1y1yHHNcjiaWbDjZZ8W3R+lwAzhzSg7hmBD4AJ/VNx+WwcbDEw/b8MgZ1r3+9Wq/MdRMmDOrGZzuPsGLbYa4Y13Amqfa5mxdYdSZdNvjxG3WCH7eCHxEREZF24/NA4R4o3AWFu5h+6EOudh7g7PcK4PUtXI4JTiBYDA27G3qNrl5Xps8pVgnl/V/C6udg3WtQsAOW3g/vPQDHT4OTroHjptQe4VO018r27F5hfT16Bpz7UIPXgrsOlwPQ/yiLlw7qESx3fajh4Mc0Td752gp+zjmxiaCsjjinnbF901m5/TArth1uJPhpusx1TRMGdeORpd+wsolMUk0bgoFVc87dWXTZ4AfDhml3YfirNO9HREREpC2YJuxbDQc3BIOc3VBgBTuU5AJmeNerAexAofW1PyWHtwtyWGMexy9uuIqEnDHgcNV/jd4nWbdpD8D6/1iB0J5PrDk8mxdahQXGzIAxV8HBjdb8nooCa5HOC/4EI7/XaPN3BBc47d/EkDewMj/Q+Fo/m/NK2Hm4HJfDxhkndG/yXHVNGNSNldsPs3JbPlef2q/Wc/mlHg6VeDCM5lVjG9M3DbfDRn6ph60HSxmc1fQx1ZXeFPwcGxxxVkUPr4IfERERkagp3ANfvQxrXrTm6jTGmQBp/ahM6sNL39jYTw/unHEu9j4nY0/O4ndzlnKgqJKpgeOY0FDgU5MrsXr9mEObrSBo7UtWVbaP/mjdQnqNsYa5ZQxs8pShYW9NzfeB6uAnr9hDcaWX+Dqj2hYFsz6nD+5Oojuyy++Jg7oxdwl8sv0IgYBZq6jBpmBmpl9GQrPO63bYObl/Osu3Hmbl9sNNBj9FFV72BedhtXmlt3ak4MdTrMyPiIiIdC2mCcX7YN8qa3HJ/V+C3YUt51TSS23Wh8POxue4NKiqHDYtsBYH3f4B4ayOM9FawyatH6T3s+5DjxO6gWGwflcB921cQZ/0eP5v6FnhU47OSeNAUS5r9xYyYVC35rel+wlWJujse63sz+rnYNt7VpsmzLS2Hy2YAnbmB4e9ZTY97C0lzkmPZDcHSzxsO1jK8J61h6eFgp9zhjd/yFvIyD5pxDvtHCmrYnNeSa0haJEMeQuZOCiT5VsPs2LrYa6Z0L/R/TYFz90rNY7UhAh/FzowBT+g4EdERESObZXFsH91MNhZZd2X5tbbzb5tKacD5h//CDnjoP9pVgW13idZi3XWZZpW5bU1L8D6N6wPlUP6fwtGXwlDv3PUudX7w5Xe4mttH52Txv++zmXN7sJIv2OLwwUnXmTdivaCpwR6DG324TsPN2/YG1jZn4Ml1nCymsHPzvwyNuWWYLcZTB7aI9LvAJfDxikDMvhwyyFWbjvc6uAnFER+suNwvUxSTZtyj735PtDlg5/gm1jBj4iIiBxLig/Alv9ZWZ29X0D+FmrOrwHAsEPWMOh9shXceCsIbP8Q79ZluL2lsH2ZdQNwxEPOKVZA028SpPaGr/9tDWs7vLX6nGl9YdSVMPoKSO/f7OaGg5/UuFrbRwUXO12zpzCS775hqX0i2r3M4+NgsMx1c4OfFdsO1yt68E6wytuEgd1ISzh6tqkhEwd148Mth1ix7TA/OG1AePuG8Jyc5ldjG9k7lSS3g8JyLz//11oeuHgE8a761edaElh1Bl08+Am+wTTnR0RERDq7yiLY8KZV8nnHR9QLdtL6Qu+xwWBnrFXW2VV7OJf/pB+waMHbnHfKIJz7PoWdH8HO5VCeDzs+tG51OROsdXZGz7ACo0jW2QlqLPMzoncqNgNyiyvJLaoku05w1JZCld7SE5zNGvYVmvezrU7Rg1CJ62ktGPIWMmGgla35dMdh/AETu82gyhdgWzDQiiRAcdht/N/5Q/m/N9bx+pf72JhbwvyrTqo3r0nBzzHIdMRhgDI/IiLHoMcff5w//OEP5ObmMmrUKB577DHGjRvX6P7z5s3jySefZPfu3WRmZnLppZcyZ84c4uLa72JLJGI+D3yzGL56Fba8A35P9XN9xsHAbwcDnrGQ1MwhV4bNGhrWeySMu9Ea2nZosxUI7VpuLQxadgj6TrSKCwy7ENytWwdmX801fmpIdDs4PiuZTbklrNlTyDmpLQ8gIhUe8taMxUABjutev+JbbnElX+4uxDBg2rCsFrflxF4pJMc5KKn0sX5/ESP7pLH1YClev0lynIM+6fFHP0kNV4zrS79uCdz64pdsPFDM9Mc+Zt7lozlriNVGf8Bkc15w/aBjaI0f6OLBD07N+RERORa98sorzJo1i/nz5zN+/HjmzZvHtGnT2Lx5Mz161L8AfPHFF7nzzjt55plnmDhxIlu2bOG6667DMAzmzp0bg+9ApAmBAOz6GNb9Czb818r4hHQfAiO+ByMujWjYWZMMA3oMsW6hYMhbblVXi5IDRVbmp3da/Yv40Tlp1cFPK7InkYpkvg9UZ352HynH4/UD8O7Gg4C1WGmPlJZ/kOKw2xg/IIN3Nx5k5bbDjOyTxqbcYGYmO+Wo6/U0ZOKgTN6+7TRufmE1X+4u5AfPfsFtZw/mp2cPZkd+GZXeAHFOW7O//84i8rzkscSuOT8iIseiuXPncuONN3L99dczbNgw5s+fT0JCAs8880yD+69YsYJJkyZx5ZVX0r9/f6ZOncoVV1zBZ5991s4tF2lC3gZYfBf86UT4x3SrglllEST3gom3wo8/gps/gdN/Hr3ApyGGEdXABxof9gZW8AOwNhrzfiKwK1jprd9RFjgN6Z7sJjnOQcCEncEhc4s3WMFPJAubNmbCoEwAVmw7DNQcltbyzEzP1Hhe+dEErplgrR/06NJv+MGzn7Nyu/UaJ2SnYG+kIEJn1cUzP8E3mLcitu0QEZGoqaqqYtWqVcyePTu8zWazMXnyZFauXNngMRMnTuT555/ns88+Y9y4cWzfvp2FCxdy9dVXN/o6Ho8Hj6d6iFFxsXUh4vV68Xq9Ebc7dExLjhXLsdCHofkc1Ru8GJsXYFv1N2y7q39/zbhUzCHTCQy/FLPvRGuoGoDP16rX93q9VPrhcEk5TkfD81xcDhtxzvoT5FuqvMpHQbn1M+ueaK/38wtVTvtqbyGVnqqjXowHAiaGQYuyITVtz7eGr+WkxTX7d2pQ90TW7Clic24xlV74bEcBAGed0K3Vv5en9E0F4POdRyiv9LB+v5XxO75HYqvObQB3n3cCI3olc/ebG/hgyyE+2HIIgCFZrTt3e4mkjV07+AlXe/M0vZ+IiHQa+fn5+P1+srJqj6/Pyspi06ZNDR5z5ZVXkp+fz2mnnYZpmvh8Pn7yk5/wq1/9qtHXmTNnDvfdd1+97YsXLyYhoXmfFDdkyZIlLT5WLC3tw8V7DZYdsHH7cD89IptCERXFVfD7tXay4uHH/fMZWfI+/fOXEecrBCCAjdzUMezNmEReyigChhPWF8P6RVF5/TIvvLjNxtcFDvjs40b3sxsmNw4JMDTNbHSfSORVADiIs5t89F79n13ABJfNTlmVn7+//j96NfH2qvLDnzfYKfPCVYP9DGjFdJUt++yAwb5NX7Jw35fNOsZdaQNsLPn0K9LdBn7TpHeCydefLOPrljcFsPoh0WH1w1/+tYivdlntO7z9KxYe/KqVZwcXcNtQeGaznXyPFTj6D+9m4cJdrT53WysvL2/2vl08+AnN+VHmR0SkK1u2bBkPPvggTzzxBOPHj2fr1q389Kc/5Te/+Q133313g8fMnj2bWbNmhb8uLi4mJyeHqVOnkpISeXUkr9fLkiVLmDJlCs5IF5cUoPV9+PDcjyjzVWD2PJHzJvZrgxY27Z2vcxm2+hWuqVzMtG++wIk1b8RM7EFgzDUExlxL95SedG+D116/v5iZL61hb+HRpwL4TYNt9OT/nTc6Kq/90dZ8WLOavt2SOe+8iQ3u81Lu53y2s4DUAaM4b2zvRs/1h8Vb2FW6E4DHNzqZfc4JXDU+J+IsUHmVj6KV7wFw5XemkBrfvN+nfR/v4NN3vsFI7clX+60qb5eeehznnTkootdvzDsla1m0Po/DSQMo9e3BZsB1F01rsFR1S11W4eVX/1nPF7sKufniceSkt/zDnPYSyrw3R5cOfsxw8KPMj4jIsSIzMxO73U5eXl6t7Xl5eWRnNzzu/u677+bqq6/mhz/8IQAjRoygrKyMH/3oR/zf//0ftgZK97rdbtzu+os+Op3OVgUvrT1eWtaHh0s97CmwPgzdU1DZvj8DTyl89QoTlj3BBe7qNXM+CwyhaPi1TP7uDdgdbqJ3eVvbq1/s4e7/fI3HFyAnPZ7L+pRw/XfPabAPNueWMP3PH/PR1ny8pkGCq/WXkodKrSFLvdPjG+33Mf3S+WxnAev2l3DlqQ3v8/W+Iv623MpSnNwvnS92FXD/gk18ta+YB787IqK27s+3fhfSEpxkpjT/4v+EbGto2voDJewrtAKu80f1jtrv06TjMlm0Po9/r94PWJXoUhKjW5Gym9PJX645BdM0Wz10sL1E0r9du+BBeJ0fZX5ERI4VLpeLsWPHsnTp0vC2QCDA0qVLmTBhQoPHlJeX1wtw7HbrUtM0ozO0Rzq2r/ZWV0wLVflqMxUFsOdz+PIFeHsWzB0KC2bRrWwr5aabL7tfxMMD/873q+7hxtX9uOmlrympjP68C4/Pz+zX13HHa1/h8QU4a0gP3rjpVHKSrHk9Dd2G906hb0YCld4AHwbnhbRWY2WuaxpzlMVOff4Ad77+Ff6Ayfkje/Kvn0zgrvOHYrcZ/GfNfr77xAp25jf/5xraN9JKZ9UV3yrwmwYDuiUwOLgtGkJFDyqC1eTacg2ezhL4RKpLZ34050dE5Ng0a9Ysrr32Wk4++WTGjRvHvHnzKCsr4/rrrwfgmmuuoXfv3syZMweA6dOnM3fuXMaMGRMe9nb33Xczffr0cBAkx7Yva1xURyX4CQSgaDfkb4X8LcHbN9Z92cH6+3c7jn94J/PHg2O5b+JE/t/o3mR/upv73lrPovW5bMkrYf7VYzk+KzprruwrrODm51exdm8RhgGzJh/PLWceh9/fdMEEwzA4Z3g2T324nUVf53LO8J6tbktTld5CRgWDny15JZRX+eplcZ5ZvoOv9xWTGu/k19NPxDAMfvitgYzoncotL37JpmDGau73RzOlGevthKq19W9mpbeQPukJuBw2qnwBAKYOy4pqEDGoeyLdk90cKrGuXYcdYwuQtocuHvwE32QqdS0icky57LLLOHToEPfccw+5ubmMHj2aRYsWhYsg7N69u1am56677sIwDO666y727dtH9+7dmT59Og888ECsvgVpZzUzCvsKKqjyBXA5IhwgE/DDyj9bC44e3tr09UVyL8gcDJnHwwnnwsAzefJ371NMJf27JWIYBled2o8Te6Vw8wur2Z5fxkWPL+f3l4xk+qheLfsmgz7+Jp9bX1pNQbmXtAQnj1w+hm8fb80k8vuPfvy0E7N46sPtLN14sGX9VEd18NP48K2eqfFkpbjJK/bw9b5ixg3ICD+363AZc5dsAeD/zh9K9+Tq4ajjB3ZjwW2nccsLq/liVwE3PvcFM888jp9NOb7JqnGhzE+/CDM/dpvBwMxENuVaC4ROHdbMhWWbyTAMJg7qxn/XWMPeWlPmuqvq4sGP1vkRETlWzZw5k5kzZzb43LJly2p97XA4uPfee7n33nvboWXS0ZimWWsNmYAJewrKGdQ9guFKpYfg9Rth+/vV2+wuyBhUHeRkHh98PBjctS9aK6r85BZb1yM1h1qN6ZvOW7eexm0vfcmKbYe59aUv2Zlfxq1nD27R9/ryZ7v51RvrCJgwvHcKT84YS05GZNmNMTnp4ezDim35nHFC6y7ww8FPatMl9kbnpPHO+jzW7CkIBz+mafKrN9ZR6Q0wcVA3vje2T73jslLieOlHp/LAgo08u2Inf35/K2v3FvLI5WPISHQ1+Fqh7N+AzMjXMxrUI4lNuSWkuUxG9I5+ZmbCwJrBjzI/kdKcH1DwIyIi0oXtPFxOUYUXl8MWnrMRyfwQdn4M80+zAh9HPJz3MNy6Gn51AG75BC77J5x9N4y6DHqfVC/wAdh1xHq91Hgn6XUuyDOT3Dz3g3H8+NsDAXhk6TfhYVWRmrtkCwETLh3bh9d+MjHiwAfAZjOYdqKVRX1nfW6L2hFimib7i44+5weqh76t3VM9P+u1VXtZvvUwboeNOd8d0egQM6fdxq+/cyKPXD6aeKedj77JZ/pjHze6cOquw5EtcFrTyf3SARib2TYFA751fHfcDhv9uiWQnRLdYgddQZcOfsLV3rwKfkRERLqqNXushSiH90oJT04PzfloUiAAHz4M/5gOpbmQeQL86H0YdyN0GwT25g+w2Znf9BwTh93GnecMId5pxxcw2VvQ/HVNQko9Pg4G54rcfcGwVi1Ues6J1lyfxevz8AdaXhTkcFkVVb4AhgHZqU1fyI+uU/TgUImH3y7YCMCsKcc3a4jahaN7859bJjEgM5F9hRV8b/5KXvpsd63CJjWzcC3J/Fx9aj/+cd1Yzs9pWYB6NL3T4nlz5mm88MPxx2xRgrbUpYMfDXsTERGRUCZhdE46/YMXu7uOVvSgLB9euBTe+w2YARh1hRX49BjaojaEhlk1dQFvGEY4E7GrOcFZHaHvKSPR1ex1axozfmAGqfFODpdV8cXOIy0+T2jIW1ZyHE5705elI/ukYRhWsYaDJZXc99Z6iiq8nNgrhRtOG9Ds1zwhO5n/zpzElGFZVPkDzH59Hb/891dUBiuo1czCpSU0PCyuKQ67jYmDunGUb6dVTshOpk8nWH+nI+riwY+GvYmIiHR1oUpvo3JSw5mXHU0Ne9u1whrmtm2pNcztwsfhoifBFXmWIHzKYGDS/yiZhlAmosn2NSKUXWrJUK66nHYbk4daQ98WtWLoWyj46dlEsYOQJLcjnJmbu3gLb391ALvN4PeXjMQRYaSREufkL1eN5Y5zTsBmwKtf7OWSJ1ew50h5dZnrFmR9pONT8AMKfkRERLooj8/Pxv3W6vBjctLDxQYazKwEAvDRXHj2Aig5YBUwuPE9GHMVtHL40dGGvYX069bMzFRDrxGaxB9hBbPGnDPcWjT4na9zW7weVnPW+KkpNPTt5c/3APDD0wYwvHdqi17bZjO4+Yzj+OcN48lIdLF+fzEXPPYxr63aB0Re5lo6h64d/Dg150dERKQr23ighCp/gIxEFzkZ8eFP+/cWlNcuKlB2GF78Piy9D0w/jLwMbnwfsoZFpR07m5n5CWemWjDsraXlmxvzrcGZJLjs7C+qZN2+oqMf0IBQ5qd3s4Of9PDjvhkJ3D75+Ba9bk2Tjsvk7VtPY1ROGkUVXt7dmAdEr5+kY+nawY89lPnRIqciIiJdSmURlOWz6ZutdKeQb2d7MYr30yNwkEHOw/TiILk7N8DhbbD1XfjLt2DrEmvUyHceg4v/Au4ISmE31RSvnwNF9ctcN6TZc5IaEMpm9c+MTkYjzmnnzGCZ60Vft2zo24GiUJnr5lUtC2V+AOZ8dwTxrugsQtwrLZ5Xf3wqM8b3DW8bqGFvx6Quvc6PGcr8+Cpi2xARERFpH4EALPgZrHoWgMuBy+OAfcCfwACW2gE78HydY7sdB9/7B2QPj2qTQkFJSpyD9ISmCxGEgqO9BRV4/YGjFgmoKZxdimJGY9rwbBasO8Cir3P5xbQTIq4+Fumwt6E9k5k15XjSE11MOi4z4vY2xe2w88DFIxg/sBsrtx1marCctxxbunTwU13tTZkfERGRLmHJ3eHAJyRgGhg2G4ZhA8NGVQB8AXA67DgdDjBsMPQCOOd3Da7R01o1h7wdLXjISnET57RR6Q2wt6Ci2aWYy2qUuY5m8HPmCd1x2W1szy9j68FSBmdF1j/hBU6bGfwYhsFtLVzgtbm+M6oX3xnVq01fQ2Kniwc/wTeaV5kfERGRY97yR2Hln63HF82ncPB3Gf2bdwFYc8+UcFnjuf/bxPwPtnHtSf2478LoZnkaEq4u1oygxDAM+ndLZFNuCTvzy5od/ISyS+kJTlKPkl2KRHKck9MGZ/LepoMs+jo3ouDH4/NzKBiQNTf4EWmtrj3nJ5T5CXgh4I9tW0RERKTtrH3ZyvoATLkfRl/B2n1WlbcBmYm11nNpTVGBlggtqNrc6mKhIGlnBPN+mrOOUEudc6JV9S3Skte5wXlOcU7bUYf7iURLFw9+akyuU7lrERGRY9M378J/b7Een3oLTLwNgDW7CwEY1ad2qeTWFBVoiV0RBib9MiNf6DRc5roNJvFPHpaFzYD1+4vZc6T5bdpfY75PpHOFRFpKwU+I5v2IiIgce/auglevgYAPRnwPpv42vCbPmj0FQO0KYlC/qEBbi3RRzdA6PZEsdFpd5jr6a9dkJLoYP6AbAO9EkP2JtMy1SDR07eDHZgdbMM2qeT8iIiLHlvyt8OL3wFsGA8+EC58Am3XpY5oma/daa9OM7pte67BQUQF/wGRvQdteH1R6/ewPl7luXmDSkoVOQ0Pr2iLzA9ULnkZS8joU/PRsZplrkWjo2sEPVGd/NOxNRETk2FGSC89fDOWHoedouOyf4Kie17PnSAVHyqpw2W0M7Vl7kn6oqABENq+mJXYHh4klxznISHQdZW9LaJ2ePRFkpiIdWhepUFnoVbsLOFjcvGuq/UWRVXoTiQYFP04FPyIiIseUymJ4/lIo3A0ZA2HGa/VKVH8ZHPI2tFcKbkf9hTLDwU8EQ8taomalt+bOe8lKjgtnpvY1IzNVXuUjr9ga3j+gjYKfnqnxjM5JwzRh8Ya8Zh0T6Ro/ItEQ9eDH7/dz9913M2DAAOLj4xk0aBC/+c1vME0zvI9pmtxzzz307NmT+Ph4Jk+ezDfffBPtpjSPMj8iIiLHDFugCvtrV0PeOkjsAVe9Dknd6+23do815G1Mnfk+IS0pKtASNdf4aS6bzaBfRnDeTzMyU6HvIS3KZa7rCg19a+68nwOa8yMxEPXg5/e//z1PPvkkf/7zn9m4cSO///3veeihh3jsscfC+zz00EM8+uijzJ8/n08//ZTExESmTZtGZWUMApBQ8ONV8CMiItKpBfyctOsv2HYtB1cyXPUaZAxocNdQsYNROakNPt+/BUUFWiLSMtchoaFvu5rRvkjWEWqNacGS1yu3HaawvKrJfU3TjHiBU5FoiHrws2LFCi688ELOP/98+vfvz6WXXsrUqVP57LPPAOuXfd68edx1111ceOGFjBw5kueee479+/fzn//8J9rNOTplfkRERDo/08S2eDa9Cz/HtDnh8heg56gGd63yBfh6v7XGz+ic9Ab36d+CogIt0dLApHpO0tEzUy0NsCI1IDORIdnJ+AIm72482OS+xRU+yqqsNRZV8EDakyPaJ5w4cSJPPfUUW7Zs4fjjj2ft2rV8/PHHzJ07F4AdO3aQm5vL5MmTw8ekpqYyfvx4Vq5cyeWXX17vnB6PB4+nuhR1cbH1H5bX68Xr9UbcxtAxXq8Xu8ONDfB5yjBbcK6uqmYfSsupH1tPfdi+1M/SIQX88MHvsa96BhMD/4VP4hj47UZ335xbQpUvQGq8s9GAIJRZCZW7dtrbZpp0aEha6PWaKzRMrjkFGarLXLdt5gesoW+bckv496q9XDq2T6P77Qtmfboluohz1p9zJdJWoh783HnnnRQXFzNkyBDsdjt+v58HHniAGTNmAJCba40DzcrKqnVcVlZW+Lm65syZw3333Vdv++LFi0lIaPmnGEuWLGFiURndgS8/X8n+reZRj5HalixZEusmHBPUj62nPmwf5eXts+K9SLNUlcGXL8DKP0PhLgDW9ZnB0GEXNXlY9ZC3tEaLDISKClR6A+wrqIhoTk5zWWWurSAg0sAktF5Pc+YkteUCp3V9/+QcHntvKyu3H2ZTbjFDslMa3E9D3iRWoh78vPrqq7zwwgu8+OKLnHjiiaxZs4bbb7+dXr16ce2117bonLNnz2bWrFnhr4uLi8nJyWHq1KmkpDT8pmqK1+tlyZIlTJkyhbji56B0I2NGDGP0yPNa1L6uqGYfOp1tN3nyWKd+bD31YfsKZd5FYqr0IHz2FHz+V6iwAhni0/F/6w52HOrN0KMc/uWeQqD+4qY1hYoKbM4rYefhsjYJfvYcKcc0IdntoFszy1yHhAKZPUfK8fkDOJrITIUCpLZY4LSuXmnxTDsxi4XrcvnHil3M+e6IBvc7EC5zrSFv0r6iHvz84he/4M477wwPXxsxYgS7du1izpw5XHvttWRnW5Ph8vLy6NmzZ/i4vLw8Ro8e3eA53W43bre73nan09mqix2n04ktWOraEagCXThFrLU/A7GoH1tPfdg+1McSU4c2W1meta+APzgcPn0ATLgFRs8gYDhh4cKjnmZtOPhpuNhBSP/MBCv4yS+DE1rb+PpCc3H6ZSY0u8x1SFZyHG6HDY8vwL7CikYzRxVVfnKD6+60R+YH4NoJ/Vm4Lpc3vtzLnecMabDCnMpcS6xEfQBreXk5Nlvt09rtdgIBaxGuAQMGkJ2dzdKlS8PPFxcX8+mnnzJhwoRoN+fonME3nc/T9H4iIiLS/kwTdi6HFy+Hx8fB6ueswKfPKfD9f8Ktq2DcjeBqXlajqMLLtkPWMLBRfdKa3DeSogIt0ZoqbDabEc7kNFWRbtcR67nUeCdpCZFll1pq3IAMhmQnU+kN8MoXuxvcZ7/KXEuMRD34mT59Og888AALFixg586dvPHGG8ydO5eLL74YsFZNvv322/ntb3/Lm2++ybp167jmmmvo1asXF110UbSbc3SOYEbJd/RFwkRERKTtbDxQzMGSYPXVgB/WvwFPnwXPngdb/gcYMOQC+ME7cMMSGPYdsEU2Wf6rvYUA9M1IoFtS/VElNfXr1vyiAgCBgMmKbfl4fP5m7R9e46eFhQiqK9I1HpyFA6x2yvqAda133cT+ADy3chf+QP051aHgp2eqgh9pX1Ef9vbYY49x9913c/PNN3Pw4EF69erFj3/8Y+65557wPnfccQdlZWX86Ec/orCwkNNOO41FixYRFxeDcZ/hUtfK/IiIiMTK1oMlTH/sY0b2SeX177hhwSw4sNZ60hEHo66ACTMh87hWvU5oyNuoJub7hPSPcKHTZ1fs5P63N/CDSQO4Z/qwo+7fkgVOa7fv6GsRtVeZ67ouHN2b3y3axN6CCt7bdJApw2oXuqoueKA5P9K+oh78JCcnM2/ePObNm9foPoZhcP/993P//fdH++UjF17kVJkfERGRWFmx7TDJgSIuO/AU/PV9a6M7FU79CZxyIyR1j8rrrGlGsYOQUGalOUUFAP6zZh8Ab67dz/+dPxS7rel5PDvzWxeYNGctovYsc11TvMvOZafk8JcPtvPsih21gh+fP0BeifWhs4a9SXtrm6L1nYkyPyIiIrEVCBC39jnec/+cy+zBwGfUlXDrF3Dmr6IW+JimyZo9RUDzgp/sFKuogC9ghtelacy+wgq+2mudO7/Uw+rdBU3u7/G1vMx1SChoampOUnWZ6/bN/ABcfWo/bAYs33qYb/JKwtsPlnjwB0ycdoPMoww9FIk2BT/OUPCjzI+IiEi727ca/no238/9I+lGKRsDOaz89vNw8ZOQ1CO6L1VYQX6pB4fN4MReR18qo2ZRgaMVPXjn69prFS76uuG1C0NCZa6T3A4yk1pWiKB/nXLXDakuc92+mR+APukJ4YzPP1buDG+vOd/HdpTsmEi0KfhR5kdERKT9lR+Bt39mFTTYv5oSM577vFdzQdWDfB4Y0iYvGRryNrRnCnHO5hVKCFd8a2JeDcCi9Vawc+rADOvrr3MxzcYXTw8NeevXLfIy1yE1M1P7g6Wja6qo8nOgKFjmOgbBD8C1wcIH/161j6IKL0A4i9YzVfN9pP0p+NGcHxERkfYTCMCXz8OfT4YvngFM8vp/h7M8D/N3/7n4sbP1YGmbvPTaCOb7hISyK01VfDtU4uHznUcA+O1FI4h32tlXWMH6/Y0vCtzaYgdQp9x1A+3bfcQKsFLiHKQ1sNZOe5gwsBsnZCVT4fXzry/2AIQDNc33kVhQ8KPMj4iISPvI/Rr+fg789xYoPwzdh8C1b/Nqzj0cIj18gd4WwU9+qYelmw4Czav0FhIKLpqq+PbuxjxME0b2SeW4HkmccYI1R6mpoW/VZa5bNxenXxNFD0JV4AZkJrY4u9RahmFwzcR+QHXZ6wNFoUpvCn6k/Sn4CS9yqsyPiIhImzm4Ef42FfZ8Cs5EmPIb+MnHMOBb4eFoF47qBcD2/FICDawN01KrdxdwwaMfs/1QGcluB6cPzmz2sQOaMewtFORMOzEbgHOGW/ehoXANqa701rrhaAOaKHcdCohiMd+npovH9CYlzsHuI+Us23ywRplrBT/S/hT8hBc5VeZHRESkTVQWwytXgbcM+k6AmZ/DpNvA7gxWYCsEYPqoXrjsNiq9gaNWV2sO0zT558qdXPaXleQWVzKoeyJv3DKRHinNn2vSL1RUoKDhogJFFV5WbMsHqoOeM4f0wGk32HqwlK0HS+odA9EZ9gZNZ6ailV1qrQSXg8tOyQGstZD2BYe9aY0fiQUFP47gpw6a8yMiIhJ9pgn/vRkOb4WU3nDZ85DaO/z03oIKDpdV4bQbDO+dGs5ktHboW0WVn//36lru/u96vH6T80Zk89+Zp3Fcj+SIztMzJQ6Xw4bX33BRgfc3HcTrNxncI4lB3ZMASIlzMuk4K7v0zvq8esd4fP5w9qO1mZ+mCjKEs0utDLCi4epT+2MY8NE3+WwL/myV+ZFYUPCjzI+IiEjbWfEYbHwLbE74/nOQWHvIWd0KbMf1sAKI1gQ/u46Uc/ETy3n9y33YbQb/d95QHr/yJJLcka/tbrMZ9MsIlbuuH2CEhryFsj4h5wSHwDU072fPkQoCJiS67C0ucx3Sv4nMVEcZ9gbQt1sCZw+xyl5XBdupam8SCwp+NOdHRESkbez4CN6913p87u+gz8n1dllTpwLboFYGP18fMbj4yU/YlFtCZpKL528Yz42nD2zVhP/GKr5VVPlZtsUqohCa7xMyeVgWNgPW7Stib0HtIWk1g5LWFiJoLDNV6fWzP1TmugNkfgCuC5a9BqsCXXJcbCrQSdem4EeZHxERkegr3g+vXQ9mAEZdASff0OBudctPhzM/hyIPfv768U6e3mynpNLHSX3TePvWbzFhULcWNb+m0JyZ0DCykA+2HKLSG6BPeny9RVMzk9yc3N9a86fu0LeaVdhaq7HMVKjMdXKcg/QYlbmua9Jx3cI/Xw15k1hR8BOe81N/HK+IiIi0gK8K/nUdlB2CrOFw/lxoIMPh9QdYt68IqC4/fVz36sxPU4uE1lVe5eOPS74B4KrxObz8owlkR2lYVWPlpN8JVnM758TsBjM4oaFv79QZ+hYqTtAvSoUIGmpfRyhzXZdhGNxw2gAAhmRHNvdKJFoiH/x6rAlnfhT8iIiIRMWSu62S1u5UuOyf4Gr4In9zbgkeX4CUOEe4pPTA7okYhlVFLb+0iu7J7ma95Nf7ivEFTFJdJvdeMBSnI3qf74bLSdcILqp8Ad7daGV06s73CZk2PJv7397A57uOcKjEE/5eolXprbp9wYVOa2SmOtJ8n5ouPyWHnqlxjOidGuumSBelzE9ozo/fY606LSIiIi237jX4dL71+Lt/gYyBje4amu8zKicNm83KTsQ57eSkWxfzkcz7WbOnAIB+SdFbHygklKHZc6Qcf3D9oZXbD1NS6aN7spuT+qY3eFzvtHhG9knFNAkHSlCzBHV0ApOGMz+hdYRiW+a6LsMwOOOEHnRLal5QKxJtCn4cNd58fs37ERERabG8DfDmrdbjb/0cTji3yd3rFjsIacm8n9C52iL46ZUaX6OogFUgKVTFbeqwrHDg1pBpdaq+VfkC7CsIlrnOjE5g0lBmaleUAyyRY4WCH0eNCXca+iYiItIylcXw6tXgLYeBZ8KZvzrqIUcLfrZFkPlZu8eaO9QvqdmHNJvNZtC3RlEBf8BkyYaGS1zXFXp+xbZ8iiq87CkoD5e57h6l7EdDmanQvKJoBVgixwoFP3YHGHbrsYoeiIiIRM404T83BRcy7QOX/A1s9iYPKa70si2Y2RlVN/jpHlm564MllewrrMAwIKcNMj9QYzHRw+Ws2lVAfmkVKXEOTh3YdDW5Qd2TGNwjCa/f5P1NB6Na5jqkZ2o8Lnt1Zsoqcx2dRVRFjjUKfqDGWj8KfkRERCK2/BHY9DbYXcGFTI9eXnrd3iJME/qkx5NZJwMS6Vo/a3YXAjC4exJxTcdcLVZd7rosPIRt8rAsnPajX0qFsj+Lvs6tnosTxYyM3WbQt1t1ZmrPkXJME5LdDjISW7eIqsixRsEPqOKbiIhIS+34EJbeZz0+9/fQZ2yzDmtsyBtUD3vLLa6kpNJ71HOt3Wuda1RO21UQCy90ml9Wq8R1c4Tm/SzbcpBNB4qB6Fdhqxmchcpc9+9AZa5FOgoFP1A970fBj4iISPMVH4DXfhBcyPRKGHt9sw9tKvhJjXeGy0JvO1RW7/nGzjWyDcsnh4aPLd+Wz77CCuKddk4/vnuzjj2xVwp90uOp9AZ466v9AOHS3tFu387D5VFfR0jkWKLgB6ozP5rzIyIi0nxL7w8uZDoCzv9jgwuZNsQ0zSaDH6ie93O0ogeBgMlXwWIHo/q0XfATCiQqvdayGGcO6U6cs3lj7AzDCGeJQsdHOzDpl1ld7nqHKr2JNErBD4AjuAK0Mj8iIiLNc3gbfPWy9Xj6I40uZNqQ/UWVHCrxYLcZDG8kW9Pcctfb80sp8fiId9oZ3KPtLvZ7pVlFBUKmNXPIW0jdqnADorTAafh8wUBnR35ZdZnrKL+GyLFAwQ+AU8GPiIhIRD74vTXc7fhzmj3PJ2RtMOszJDu50ezJcc0sevBlsNjBiN6pOJpRfKCl7DaDnAxrmLzLbuOsIT0iOv6kvunhoXwJLnv4cbRUl7uuYPuhUOZHw95E6lLwA8r8iIiIROLQZlj3L+vxGbMjPvxoQ96g+Wv9hM/Vt/FzRUsoWzPpuG4kxzkjOtZmM5g6LAuIbpnrkFBmqsof4ECRdT2jzI9IfQp+oDr40ZwfERGRowtlfU44H3qNjvjwUGnquuv71BQKfnYdKafKF2h0v3Cltz6Nnytapg7Lxmk3uHZi/xYdf8W4viS47OEgKJpqZqYAktwOuqnMtUg9jlg3oENQ5kdERKR5Dm6Er1+3Hp9xZ8SH+/wB1u2zChSMaSL46ZHsJtntoMTjY+fhMo7PSq63T6XXz6YDJUD7ZH6+f0oOl47tg83WsqzN8N6pfP3raS0+/mj6d0sMV8frn5mgMtciDVDmBzTnR0REpLmW/Q4wYeh3oOfIiA/fkldKhddPktvBoGBFt4YYhnHUxU6/3leEL2DSPdlNr9S4iNvSEq0NXNoq8IHaw9yivY6QyLFCwQ8o8yMiItIcuV/Dhv8ARovm+kCNNXn6pB41EDha0YPQuUb1SVOWg9oFDlTsQKRhCn5Ac35ERESaY9kc6/7EiyBrWItOsbYZxQ5Cmhv8jGmHIW+dQc3Mj9b4EWmYgh9Q5kdERORoDqyFTW8DBnw78rk+Ic2p9BYSWuj0aMFPc87VFdQMeFTpTaRhCn5Ac35ERESOZtnvrPsRl0KPIS06RanHx5aDwQIFEWR+tueXEgiYtZ7LL/Wwt6ACw4ARfRpeKLWr6ZUWT0qcA6fdCAeOIlKbqr2BMj8iIiJN2bcaNi8Ewwbf/mWLT7NubxGmCb1S4+iRcvQCBTkZCbgcNiq9AfYVVpCTUT2PJTR8blD3JFIiXHPnWGW3Gbzww1Mpq/KRrjLXIg1S5gc050dERKQpobk+I74PmYNbfJpIFyS12wwGBodv1R36piFvDRvRJ5VTB3aLdTNEOiwFP6DMj4iISGP2fgHfLAbDDt++o1WnWrOnAIhsQdLGyl2HK70p+BGRCCj4Ac35ERERacz7D1r3o66AboNadaq1e6zFTSPJ1jRU9CAQMMPD3ppaKFVEpC4FP6DMj4iISEN2fwLbloLNAaf/vFWnyi2qJLe4EluEBQrC5a4PVQc/Ow6XUVzpw+2wcUJ2cqvaJSJdi4IfqBH8eGLbDhERkY4klPUZfSVkDGjxacqrfPxmwQYAjs9KJsHV/HpLNdf6MU2r4tua3YUADO+ditOuSxkRaT5Ve4MaBQ8qYtsOERGRjmLnctjxAdic8K2WZ322HyrlpudXszmvBIfNYOZZx0V0/IDMRGwGFFV4yS+tonuym7V7CwEVOxCRyOnjEqgx50eZHxEREaC6wttJV0N6vxad4p31uVz45+Vsziuhe7Kbl350KheM7BXROeKc9nCJ69C8HxU7EJGWUvADNYa9KfMjIiLCjg9h50dgd8G3/l/Eh/v8AX6/aBM//ucqSjw+xvXPYMGtp3FK/4wWNSdc9OBQKZVePxsPFAMqdiAikdOwN9CcHxERkRDTrJ7rc9K1kNonosMPl3q47eUvWb71MAA3nDaAO88d0qq5Ocf1SGLppoNsO1jKhgPFeP0m3RJd9EmPb/E5RaRrUvADmvMjIiISsn0Z7F4Jdjd8a1ZEh67ZU8hNz6/iQFElCS47v79kJNNHRTbMrSE11/oJFTsYlZOGYRitPreIdC0KfkBzfkREREI++L11f/IPIMUKXAIBk8fe28rfV+yg0utv9FCPL4BpwsDMROZfPZbjs6JThnpQjbV+MhJdgIodiEjLaM4P1J7zEyyjKSIindvjjz9O//79iYuLY/z48Xz22WdN7l9YWMgtt9xCz549cbvdHH/88SxcuLCdWttB5H9jZX0MO0z6KQCF5VX84B+f86d3t1BY7qXSG2j0Zppw7vBs/jtzUtQCH6gud51bXMnK7dZwOgU/ItISyvwAONzVj/1Vtb8WEZFO55VXXmHWrFnMnz+f8ePHM2/ePKZNm8bmzZvp0aNHvf2rqqqYMmUKPXr04LXXXqN3797s2rWLtLS09m98LK192bo/bjKk9OTrfUX85PlV7C2oIM5p4/4LhzNxULdGD3c5bPRIjot6s1LjnXRPdnOoxMOhEmuUxqg+aVF/HRE59in4AXDUmDDprVDwIyLSyc2dO5cbb7yR66+/HoD58+ezYMECnnnmGe688856+z/zzDMcOXKEFStW4HQ6Aejfv397Njn2AgH46lXr8ajLePWLPdz9n6/x+AL0zUhg/lVjGdYrJWbNO657UjjwGZiZSGqCM2ZtEZHOS8EPgN0JGICpeT8iIp1cVVUVq1atYvbs2eFtNpuNyZMns3LlygaPefPNN5kwYQK33HIL//3vf+nevTtXXnklv/zlL7Hb7Q0e4/F48Hiq/2YUF1vll71eL16vN+J2h45pybHRYOxajqNoN6Yrmbs25PDC6q8AOPOETP5wyQhS450xaxvAwMyE8JC3kb1TGmxLrPvwWKA+jA71Y/uKpJ8V/AAYBjjjwVuutX5ERDq5/Px8/H4/WVlZtbZnZWWxadOmBo/Zvn077733HjNmzGDhwoVs3bqVm2++Ga/Xy7333tvgMXPmzOG+++6rt33x4sUkJCS0uP1Llixp8bGtMXrXX+kHvO0dywurD2Fgcm5OgCnpuSx/Pzcmbaqp8qABWIGovWgvCxfuaXTfWPXhsUR9GB3qx/ZRXl7e7H0V/IQ43MHgR5kfEZGuJhAI0KNHD5566insdjtjx45l3759/OEPf2g0+Jk9ezazZlWXgi4uLiYnJ4epU6eSkhL58DCv18uSJUuYMmVKeOhdu/FWYPzpJgCeqzydtHgnc783gm8NzmzfdjQhbdth/v3sKgCunDaRkX1S6+0T0z48RqgPo0P92L5CmffmUPAT4ogHCrTWj4hIJ5eZmYndbicvL6/W9ry8PLKzsxs8pmfPnjidzlpD3IYOHUpubi5VVVW4XK56x7jdbtzu+nNEnU5nqy52Wnt8S5R/9ToJ3jL2BLrj6XkKb111MjkZLc9etYXhfdJxOWzEOWwMz0nH6Wh4OCLEpg+PNerD6FA/to9I+lilrkNCRQ6U+RER6dRcLhdjx45l6dKl4W2BQIClS5cyYcKEBo+ZNGkSW7duJRAIhLdt2bKFnj17Nhj4HGv2LHsGgPfjzuTVmyZ1uMAHoFuSmxd+OJ4XbzwVdxOBj4hIUxT8hDiDFd8050dEpNObNWsWTz/9NP/4xz/YuHEjN910E2VlZeHqb9dcc02tggg33XQTR44c4ac//SlbtmxhwYIFPPjgg9xyyy2x+hbazar1mxhUbK2BNOr8nxDn7LiBxSn9Mxjeu/5wNxGR5tKwtxBlfkREjhmXXXYZhw4d4p577iE3N5fRo0ezaNGicBGE3bt3Y7NVf/6Xk5PDO++8w89+9jNGjhxJ7969+elPf8ovf/nLWH0L7aLS6+eT//6FsUaA3QknMmrU2Fg3SUSkTbVJ5mffvn1cddVVdOvWjfj4eEaMGMEXX3wRft40Te655x569uxJfHw8kydP5ptvvmmLpjRfaK0fzfkRETkmzJw5k127duHxePj0008ZP358+Llly5bx7LPP1tp/woQJfPLJJ1RWVrJt2zZ+9atfNVrm+ljx2HvfcEalNTyw+2nXxrg1IiJtL+rBT0FBAZMmTcLpdPK///2PDRs28Mc//pH09PTwPg899BCPPvoo8+fP59NPPyUxMZFp06ZRWVkZ7eY0nzI/IiLShWzYX8z7Hy7jRNsuAoaT+NHfi3WTRETaXNSHvf3+978nJyeHv//97+FtAwYMCD82TZN58+Zx1113ceGFFwLw3HPPkZWVxX/+8x8uv/zyeudsj4Xk7HY3NsDnKcXUglRHpcW7okP92Hrqw/alfj42+AMmd77+Fd8xPgLAdsI0SMiIcatERNpe1IOfN998k2nTpvG9732PDz74gN69e3PzzTdz4403ArBjxw5yc3OZPHly+JjU1FTGjx/PypUrGwx+2mMhuZMPHaE3sOGr1ew40HHWNejotHhXdKgfW0992D4iWUhOOq6/L9/B13sL+GvccmvDqCti2yARkXYS9eBn+/btPPnkk8yaNYtf/epXfP7559x22224XC6uvfZacnOtVaIbWnk79Fxd7bGQnP2thVD4GSceP4ihE86L+JxdjRbvig71Y+upD9tXJAvJSce050g5f1y8hUm2r+lBAcSnw+CpsW6WiEi7iHrwEwgEOPnkk3nwwQcBGDNmDF9//TXz58/n2mtbNpmyXRaSC5a6tge82HUB1WxavCs61I+tpz5sH+rjzs00TX71xjoqvH5+lP4ZVADDLwHHsb+WkYgItEHBg549ezJs2LBa24YOHcru3bsBwqtrR7LydrsIr/MTw6ILIiIibeiNL/fx0Tf5pDs8TPKutDZqyJuIdCFRD34mTZrE5s2ba23bsmUL/fr1A6ziB9nZ2bVW3i4uLubTTz9tdOXtdhGu9qbgR0REjj35pR7uf3sDAA8P34XNVwHdjoPeWttHRLqOqA97+9nPfsbEiRN58MEH+f73v89nn33GU089xVNPPQWAYRjcfvvt/Pa3v2Xw4MEMGDCAu+++m169enHRRRdFuznN51DmR0REjk2lHh//79W1FJZ7GdozhTM971tPjLwcDCO2jRMRaUdRD35OOeUU3njjDWbPns3999/PgAEDmDdvHjNmzAjvc8cdd1BWVsaPfvQjCgsLOe2001i0aBFxcXHRbk7zhTI/XgU/IiJy7Nh6sIQf/3MV2w6V4bLbmDutG7aXP7SeHPn92DZORKSdRT34Abjgggu44IILGn3eMAzuv/9+7r///rZ4+ZbRnB8RETnGLFx3gF/8ay1lVX6yU+J44qqTGLrr74AJ/U6D9H6xbqKISLtqk+CnU9KcHxEROUb4/AF+v2gTT3+0A4BTB2bw5ytPIjPRBW++bO006rIYtlBEJDYU/IRozo+IiBwDDpV4mPniaj7dcQSAH58+kF9MOwGH3Qb7v4T8zeCIg2EXxrilIiLtT8FPiOb8iIhIJ7dq1xFufmE1ecUeEl12Hv7eKM4d0bN6h7XBrM+Q8yEuNTaNFBGJIQU/IY5gsQVlfkREpBP61xd7mP36OnwBk+N6JDH/qrEc1yOpege/F9a9Zj3W2j4i0kUp+AlxKvgREZHOyR8wuffN9fgCJueP7MlDl4wk0V3nT/zWpVCeD4k9YOCZsWmoiEiMKfgJUeZHREQ6qQNFFZRX+XHZbTx6+RjstgbW7ln7knU/4ntg159/EemabLFuQIcRCn4050dERDqZnfnlAORkxDcc+FQUwub/WY9HXd5+DRMR6WAU/IQo8yMiIp3UzsNlAPTvltjwDhv+A34P9BgG2SPar2EiIh2Mgp8QzfkREZFOame+Ffz0ayz4CVV5G3U5GA1khkREuggFPyE1Mz+mGdu2iIiIRGDnYWvY24DMhPpPHtkBu1eCYYMR32/nlomIdCwKfkJCwY8ZsMqBioiIdBK7DjeR+flmsXXf/zRI6Vn/eRGRLkTBT0go+AENfRMRkU4jEDDZdSSU+Wkg+Nn5sXU/4Nvt2CoRkY5JwU+Iw139WMGPiIh0EgeKK6nyBXDaDXqmxtV+0jRh1wrrcf/T2r9xIiIdjIKfEMNQxTcREel0QsUOcjIScNjr/FnP32ItbOqIg15jYtA6EZGORcFPTVrrR0REOpkmy1zvWm7d9zml9ggHEZEuSsFPTcr8iIhIJ1Nd5rqBSm87g8FPv0nt2CIRkY5LwU9N4bV+PLFth4iISDNVl7muk/mpNd9HwY+ICCj4qS2c+amIbTtERESaqdEy1wU7oGQ/2JzQ++QYtExEpONR8FOTQ5kfERHpPAIBk12hzE/d4CeU9ek9FlwNDIkTEemCFPzUFC54oMyPiIh0fLnFlXh8ARw2g15pdcpch+f7TGz/homIdFAKfmrSnB8REelEQsUO+jZU5jpU6U3zfUREwhT81KQ5PyIi0omEih3Uq/RWtBcKd4Fhh5zxMWiZiEjHpOCnJs35ERGRTmRnY8UOQvN9eo4Cd3I7t0pEpONS8FOT5vyIiEgnEhr2Vq/M9c6PrXvN9xERqUXBT02a8yMiIp3IrsaGvYXX9zmtnVskItKxKfipSXN+RESkkwgEzPCwt1qZn5I8OPwNYEDfU2PTOBGRDkrBT00Ot3WvzI+IiHRweSXVZa57p8VXPxGq8pY1HOLTY9M4EZEOSsFPTY7gHw/N+RERkQ5uR3C+T07dMtehIW+a7yMiUo+Cn5qU+RERkU6i8fk+Wt9HRKQxCn5qcgYzP5rzIyIiHVxovk//mmWuy4/AwQ3W477K/IiI1KXgpyZlfkREpJMIlbnuXzPzExrylnkCJHWPQatERDo2BT81ac6PiIh0EuFhbzUrvWm+j4hIkxT81KTMj4iIdAK1ylzXHPa2K7i4qdb3ERFpkIKfmjTnR0REOoGDJR4qvQHsNoPe6cG/XZVFkLvOeqzMj4hIgxT81KTMj4iIdALhMtfp8ThDZa53fwpmANIHQEqvGLZORKTjUvBTk+b8iIhIJ7ArOOStX60hb8ES1/1U4lpEpDEKfmpS5kdERDqBncFiB7UrvWl9HxGRo1HwU1N4zk9lbNshIiLShHCZ61Clt6oy2P+l9ViZHxGRRin4qSmc+VHwIyIiHVe9BU73fAYBH6T0gbS+MWyZiEjHpuCnJocyPyIi0rGZphle4yec+Qmt79N/EhhGjFomItLxKfipKZT5CfjA74ttW0RERBpwsMRDhdeP3WbQJ1TmOlzsQCWuRUSaouCnptCcH1D2R0REOqRQmes+oTLX3krY+4X1ZD8tbioi0hQFPzXZ3dWPFfyIiEgHVK/M9b5V4PdAYg/oNiiGLRMR6fgU/NRks1UHQAp+RESkAwqVuR4QKnOt+T4iIs2m4KcuR5x171XwIyIiHU+ozHU487PrY+teJa5FRI5KwU9dzmDwo8yPiIh0QOEFTjMTwO+1ylyDgh8RkWZQ8FOX1voREZEOyipzXWONn/1rwFsO8enQfUhsGyci0gko+KlLa/2IiEgHdajEQ3mVH5sBfdITapS4nmTNWxURkSbpf8q6QpkfzfkREZEOprrMdQIuh03r+4iIREjBT10OzfkREZGOaVdwvk+/bgkQ8MPuT6wnNN9HRKRZFPzUpYIHIiLSQe0MzvcZkJkIuevAUwzuFMgeEeOWiYh0Dgp+6lLmR0REOqidNRc4Da3v0/dUsNlj2CoRkc5DwU9d4XV+KmLbDhERkTp25gfLXHdL0HwfEZEWaPPg53e/+x2GYXD77beHt1VWVnLLLbfQrVs3kpKSuOSSS8jLy2vrpjRPOPPjiW07REREajBNM5z56d8tvjrz0++0GLZKRKRzadPg5/PPP+cvf/kLI0eOrLX9Zz/7GW+99Rb/+te/+OCDD9i/fz/f/e5327IpzRee86PMj4iIdByHSqvLXPf17YaKI+BMgF6jY900EZFOw9FWJy4tLWXGjBk8/fTT/Pa3vw1vLyoq4m9/+xsvvvgiZ511FgB///vfGTp0KJ988gmnnnpqvXN5PB48nupMTHFxMQBerxev1xtx20LHNHSszebCDvg9FQRacO6uoqk+lOZTP7ae+rB9qZ9jJ1TprXd6PM69K62NOePA7oxhq0REOpc2C35uueUWzj//fCZPnlwr+Fm1ahVer5fJkyeHtw0ZMoS+ffuycuXKBoOfOXPmcN9999XbvnjxYhISElrcxiVLltTbNmzffgYDO77ZwPryhS0+d1fRUB9K5NSPrac+bB/l5eWxbkKXFVrjp3/NYgcqcS0iEpE2CX5efvllVq9ezeeff17vudzcXFwuF2lpabW2Z2VlkZub2+D5Zs+ezaxZs8JfFxcXk5OTw9SpU0lJSYm4fV6vlyVLljBlyhScztqfmNk+WAsHFzIgpxf9zjkv4nN3FU31oTSf+rH11IftK5R5l/a363CN4OfAN9bGnqNj1yARkU4o6sHPnj17+OlPf8qSJUuIi4uLyjndbjdut7vedqfT2aqLnQaPdycCYA9UYdeF1FG19mcgFvVj66kP24f6OHZCld76dUuArQetjclZMWyRiEjnE/WCB6tWreLgwYOcdNJJOBwOHA4HH3zwAY8++igOh4OsrCyqqqooLCysdVxeXh7Z2dnRbk7ktM6PiIh0QOFKbxnxUJZvbUzsEcMWiYh0PlHP/Jx99tmsW7eu1rbrr7+eIUOG8Mtf/pKcnBycTidLly7lkksuAWDz5s3s3r2bCRMmRLs5kVPwIyIiHYxpmuwMzvkZmFQFph8wIDEztg0TEelkop75SU5OZvjw4bVuiYmJdOvWjeHDh5OamsoNN9zArFmzeP/991m1ahXXX389EyZMaLDYQbsLL3Kq4EdEpDN7/PHH6d+/P3FxcYwfP57PPvusWce9/PLLGIbBRRdd1LYNjEBBuZeyKj8AfZzBeVcJGar0JiISoTZf5LQhf/rTn7jgggu45JJLOP3008nOzub111+PRVPqcyrzIyLS2b3yyivMmjWLe++9l9WrVzNq1CimTZvGwYMHmzxu586d/PznP+db3/pWO7W0eY6UWcs9pMY7cVVoyJuISEu1WanrmpYtW1br67i4OB5//HEef/zx9nj5yGjYm4hIpzd37lxuvPFGrr/+egDmz5/PggULeOaZZ7jzzjsbPMbv9zNjxgzuu+8+Pvroo3pzU+tqzzXoDhVbC2+nxTvxFR/AAQQSM/Fr3aVatO5X66kPo0P92L4i6ed2CX46FQU/IiKdWlVVFatWrWL27NnhbTabjcmTJ7Ny5cpGj7v//vvp0aMHN9xwAx999NFRX6c916Bbd8QA7FBVxqYvPmQ4sL/Iy6qFWo+uIVr3q/XUh9GhfmwfkaxBp+CnLs35ERHp1PLz8/H7/WRl1S4DnZWVxaZNmxo85uOPP+Zvf/sba9asafbrtOcadGWr9sLmDQzo1Z2hvbrBPug5eDTnTdF6dDVp3a/WUx9Gh/qxfUWyBp2Cn7rCc348Te8nIiLHhJKSEq6++mqefvppMjObXz2tPdegK/YEAMhIcmOvOAyAPSVb69E1Qut+tZ76MDrUj+0jkj5W8FNXeNhbRWzbISIiLZKZmYndbicvL6/W9sbWk9u2bRs7d+5k+vTp4W2BgBVsOBwONm/ezKBBg9q20UdRUF4FQHqCCwqCRRtU8EBEJGIxqfbWoTmU+RER6cxcLhdjx45l6dKl4W2BQIClS5c2uJ7ckCFDWLduHWvWrAnfvvOd73DmmWeyZs0acnJy2rP5DSoos4KfjEQXlAWDnyQFPyIikVLmp67wnB9lfkREOqtZs2Zx7bXXcvLJJzNu3DjmzZtHWVlZuPrbNddcQ+/evZkzZw5xcXEMHz681vFpaWkA9bbHSkG5VckoLcEJpYesjYndY9giEZHOScFPXc546z7ghYAfbPbYtkdERCJ22WWXcejQIe655x5yc3MZPXo0ixYtChdB2L17NzZb5xn8UBga9hbvgLJg8JOU1cQRIiLSEAU/dTlqTF71VYIrMXZtERGRFps5cyYzZ85s8Lm668/V9eyzz0a/Qa1wJDjsLdNeCqbf2pjY/OIMIiJi6Twfe7WX0LA30LwfERHpEAqDw94yjSJrQ3wG2FVBSkQkUgp+6rLZwRb8g6J5PyIiEmOmaVJYYQU/6YFCa6OKHYiItIiCn4aEK75poVMREYmt4kof/oAJQJKvwNqoYgciIi2i4KchTgU/IiLSMYTKXCe67DjLVexARKQ1FPw0RJkfERHpIEILnKYlaI0fEZHWUvDTkPBaPwp+REQktkLFDtITtcaPiEhrKfhpiDI/IiLSQYTKXKcr8yMi0moKfhqiOT8iItJBhIa9pSe4oDTP2qg5PyIiLaLgpyHK/IiISAcRHvaWoGFvIiKtpeCnIeHgR4uciohIbB0JFTyId0BZqNqbhr2JiLSEgp+GONzWvRY5FRGRGCsMBj89XRVg+q2NyvyIiLSIgp+GOOOte2V+REQkxgrKrGFvPWxF1ob4dLA7Y9giEZHOS8FPQ0KZH58yPyIiEluhggeZBIMfFTsQEWkxBT8NcSjzIyIiHUN4kdNAobVBQ95ERFpMwU9DNOdHREQ6ANM0KQhWe0vxH7E2qtiBiEiLKfhpiOb8iIhIB1Dh9VPlCwCQ4A0GP4kKfkREWkrBT0M050dERDqAI2XWkDeX3YazMt/amKRhbyIiLaXgpyGa8yMiIh1AeIHTRCdG6UFrowoeiIi0mIKfhmjOj4iIdAChYgfpCS4IBT8a9iYi0mIKfhqiOT8iItIBhIodpCU4oeyQtVHD3kREWkzBT0Mccda95vyIiEgMFQTn/GTEO6qDH2V+RERaTMFPQ8LBjzI/IiISO6Fhbz3jPBDwWRu1zo+ISIsp+GlIKPjRnB8REYmhUMGD3o5ia0N8OjhcMWyRiEjnpuCnIeFS18r8iIhI7IRKXWfZgsGPhryJiLSKgp+GhAseKPMjIiKxExr2lmkUWRuSFPyIiLSGgp+GKPMjIiIdQHidH7PQ2qD5PiIiraLgpyGhRU69lbFth4iIdGmhYW8pviPWBmV+RERaRcFPQ8KZHwU/IiISO4XBYW+JXgU/IiLRoOCnIaE5P34PBAKxbYuIiHRJVb4AZVV+AOKqgsGPCh6IiLSKgp+GhDI/YAVAIiIi7SyU9bEZ4KgILnCqzI+ISKso+GlIaM4PaOibiIjExJFg8JOW4MIoDQY/KnggItIqCn4aYneAYbceq+iBiIjEQEGZVektI94OZQetjUlZMWyRiEjnp+CnMeG1fhT8iIhI+wsNe+sTXwUBn7VRmR8RkVZR8NMYVXwTEZEYCg17y3GVWhvi0sDhil2DRESOAQp+GuNQ5kdERGIntMBpH2eJtUHFDkREWk3BT2NCmR/N+RERkRgoCC5wmmUPBj8qcy0i0moKfhqjOT8iIhJDBcHMT3cKrQ3K/IiItJqCn8Zozo+IiMRQQXDOT7pZaG1Q8CMi0moKfhqjOT8iIhJDoeAnJVBgbVClNxGRVlPw0xjN+RERkRgKFTxI8gaDH2V+RERaTcFPYzTnR0REYuhIsOBBXNVha4MKHoiItJqCn8Zozo+IiMSIP2BSXGllfpwVh6yNyvyIiLSagp/GaM6PiIjESFGFF9MEMLGV51sbFfyIiLSagp/GaM6PiIjESGjIW+84D0bAygCp4IGISOtFPfiZM2cOp5xyCsnJyfTo0YOLLrqIzZs319qnsrKSW265hW7dupGUlMQll1xCXl5etJvSOprzIyIiMVIYrPQ2MK7M2hCXWv2hnIiItFjUg58PPviAW265hU8++YQlS5bg9XqZOnUqZWVl4X1+9rOf8dZbb/Gvf/2LDz74gP379/Pd73432k1pHc35ERGRGAktcNrPXWptULEDEZGocET7hIsWLar19bPPPkuPHj1YtWoVp59+OkVFRfztb3/jxRdf5KyzzgLg73//O0OHDuWTTz7h1FNPjXaTWsYRZ90r+BERkXYWWuOnt7PE2pCUFcPWiIgcO6Ie/NRVVFQEQEZGBgCrVq3C6/UyefLk8D5Dhgyhb9++rFy5ssHgx+Px4PF4wl8XFxcD4PV68Xq9EbcpdExTx9psTuxAoKocfwte41jXnD6Uo1M/tp76sH2pn9tHQXDOT7Y9FPxovo+ISDS0afATCAS4/fbbmTRpEsOHDwcgNzcXl8tFWlparX2zsrLIzc1t8Dxz5szhvvvuq7d98eLFJCQktLh9S5YsafS5AYe2MRI4sGcnXyxc2OLXONY11YfSfOrH1lMfto/y8vJYN6FLCA17625YHyBq2JuISHS0afBzyy238PXXX/Pxxx+36jyzZ89m1qxZ4a+Li4vJyclh6tSppKSkRHw+r9fLkiVLmDJlCk6ns8F9jDVHYO8/6ZmZxnnnndfith+rmtOHcnTqx9ZTH7avUOZd2lao4EG6WWhtUOZHRCQq2iz4mTlzJm+//TYffvghffr0CW/Pzs6mqqqKwsLCWtmfvLw8srOzGzyX2+3G7a5f5cbpdLbqYqfJ492JANgCVdh0QdWo1v4MxKJ+bD31YftQH7ePUKnr1ECBtUGZHxGRqIh6tTfTNJk5cyZvvPEG7733HgMGDKj1/NixY3E6nSxdujS8bfPmzezevZsJEyZEuzktFy544Gl6PxERkSgrDA57S/IesTao4IGISFREPfNzyy238OKLL/Lf//6X5OTk8Dye1NRU4uPjSU1N5YYbbmDWrFlkZGSQkpLCrbfeyoQJEzpOpTeoDn68FbFth4iIdDmham/xVYetDRr2JiISFVEPfp588kkAzjjjjFrb//73v3PdddcB8Kc//QmbzcYll1yCx+Nh2rRpPPHEE9FuSus4lfkREZHYsIIfE2dlMPOjYW8iIlER9eDHNM2j7hMXF8fjjz/O448/Hu2Xj57wsDdlfkREpP2YpklhuZcUyrAFrAwQicr8iIhEQ9Tn/BwzNOdHRERioNTjwxcwq8tcx6VWj0YQEZFWUfDTGM35ERGRGAit8dPbEVzgVEPeRESiRsFPYzTnR0REYiAU/PRzl1obkhT8iIhEi4KfxtSc89OMeUwiIiLREFrgtLcrGPxovo+ISNQo+GmMo8b4an9V7NohIiJdSmiNn5724LA3ZX5ERKJGwU9jagY/mvcjIiLt5Egw+OluFFobFPyIiESNgp/G2J1gBLtH835ERKSdhDI/3QhWe1PBAxGRqFHw0xjD0Fo/IiLS7gorrKHWqYECa4MyPyIiUaPgpyla60dERNpZKPOT5AsGP8r8iIhEjYKfpmitHxERaWdWqWuT+KrD1oYkVXsTEYkWBT9N0Vo/IiLSzgrKvaRQhj1gZYCU+RERiR4FP03RnB8REWlnheVVdDeCxQ7cqdUfxImISKsp+GmK5vyIiEg7Kyj3kkmx9YWGvImIRJWCn6Zozo+IiLSjKj94fAEyDZW5FhFpCwp+mqI5PyIi0o7KfNZ9tj0Y/CjzIyISVQp+mqI5PyIindbjjz9O//79iYuLY/z48Xz22WeN7vv000/zrW99i/T0dNLT05k8eXKT+7eVUPDT21liPUjKavc2iIgcyxT8NMXhtu6V+RER6VReeeUVZs2axb333svq1asZNWoU06ZN4+DBgw3uv2zZMq644gref/99Vq5cSU5ODlOnTmXfvn3t2u4yrwFAT3sw+NGwNxGRqHLEugEdmiPeutecHxGRTmXu3LnceOONXH/99QDMnz+fBQsW8Mwzz3DnnXfW2/+FF16o9fVf//pX/v3vf7N06VKuueaaBl/D4/Hg8VR/OFZcbBUp8Hq9eL3eiNvs9XrDmZ8eNmvYmy8+A7MF5+qqQv3ekv4Xi/owOtSP7SuSflbw0xRlfkREOp2qqipWrVrF7Nmzw9tsNhuTJ09m5cqVzTpHeXk5Xq+XjIyMRveZM2cO9913X73tixcvJiEhIfKGA2U+K/OT7M0HYNWm3eQeWNiic3VlS5YsiXUTOj31YXSoH9tHeXl5s/ftcsFPmcfHn5Zs4f2v7UydFsDpbGJnZzDz46tsl7aJiEjr5efn4/f7ycqqPV8mKyuLTZs2Nescv/zlL+nVqxeTJ09udJ/Zs2cza9as8NfFxcXh4XIpKSkRt9vr9bLomXcB6GEvhQCMPf08zN4nRXyursrr9bJkyRKmTJmCs8k/8NIY9WF0qB/bVyjz3hxdLviJc9r516q9FFUYfL2/mFMGNlFJJ5z5UfAjItJV/O53v+Pll19m2bJlxMU1vsCo2+3G7XbX2+50Olt8sWNlfkySfAUAONJ60vSndNKQ1vwMxKI+jA71Y/uIpI+7XMEDu81gXH9rGMPK7Uea3tmhzI+ISGeTmZmJ3W4nLy+v1va8vDyys7ObPPbhhx/md7/7HYsXL2bkyJFt2cwGlfkghXIcZnD8ugoeiIhEVZcLfgAmDrKCn0+OGvwEP9HzKvgREeksXC4XY8eOZenSpeFtgUCApUuXMmHChEaPe+ihh/jNb37DokWLOPnkk9ujqfWUeale4NSdUr3enIiIREWXG/YGcOoAK/hZtbuQSq+fOKe94R0150dEpFOaNWsW1157LSeffDLjxo1j3rx5lJWVhau/XXPNNfTu3Zs5c+YA8Pvf/5577rmHF198kf79+5ObmwtAUlISSUlJ7dbuMp9BL4LBT6IWOBURibYuGfwM6p5IitOk2Btg9e4CJg7KbHhHzfkREemULrvsMg4dOsQ999xDbm4uo0ePZtGiReEiCLt378Zmqx788OSTT1JVVcWll15a6zz33nsvv/71r9ut3eW+GpkfLXAqIhJ1XTL4MQyDwakmq/INVmw93ETwo8yPiEhnNXPmTGbOnNngc8uWLav19c6dO9u+Qc1QWiv4UeZHRCTauuScH4DjU00AVmzLb3wnzfkREZF2UuUL4PEb1cGPih2IiERdlw9+1u4toqSykVVhNedHRETaSVGF9beoezjzo+BHRCTaumzwk+GGvhnx+AMmn+9spOqb5vyIiEg7KSy3gp+e9uBifSp4ICISdV02+AGYMNCq+rZ86+GGd9CcHxERaSdHyqsA6GELBj8qeCAiEnVdPPjpBsCKbY0FP5rzIyIi7SOU+elGobVBw95ERKKuSwc/4wekA7DxQDGHSz31d9CcHxERaSeFFV7AJD1QaG3QsDcRkajr0sFPZpKbE7KSAfhkewPzfjTnR0RE2klBWRXJVOAkWIRHmR8Rkajr0sEPwMTjrKFvyxsqeV1zzo9ptmOrRESkqyms8NLdKLS+cCVXjz4QEZGoUfATXOB0ZUPzfkKZHzMA/kbKYYuIiERBQbmXTFTmWkSkLXX54Gf8wAxsBuzIL2N/YUXtJ2t+6qahbyIi0oYKy73VC5wq+BERaRNdPvhJiXMyok8a0EDVN7sLMKzHCn5ERKQNFZRXVQc/KnYgItImunzwAzBpULDk9dY6834MAxxx1mMFPyIi0oaU+RERaXsKfqie97Ni22HMuoUNtNaPiIi0g8IKL91Dc34SFfyIiLQFBT/Ayf3Tcdlt5BZXsj2/rPaTyvyIiEgb8wdMCiuU+RERaWsKfoA4p52T+qUBDcz7cYaCnwYWQRUREYmC4govpgndFfyIiLQpBT9Bk0JD3+rO+wlnfupUghMREYmSgvIqoEbwo2FvIiJtQsFPUGix05XbDxMI1Jj341DmR0RE2pYV/Jg11vlRtTcRkbag4CdoZJ80El12Csu9bDhQXP1EKPjxKvMjIiJto1+3RB65cABuI7igtjI/IiJtQsFPkNNuY9yADABW1pz3ozk/IiLSxjKT3Jw3wPqTbLqSwJUQ4xaJiBybFPzUMOk4a97P8m015v1ozo+IiLQDo+yQ9UALnIqItBkFPzVMCC52+tmOI3j9AWuj5vyIiEh7CAY/poa8iYi0GQU/NQzNTiE9wUl5lZ+1ewqtjZrzIyIi7cAoPWg9UOZHRKTNKPipwWYzwtmf8Ho/mvMjIiLtIZT50Ro/IiJtRsFPHROD6/0sD633ozk/IiLSDowyZX5ERNqaI9YN6GgmBjM/X+4uZPnWfIZ77aQC7PkMVj4BNjsYNjCM4L0NDHuNxzaIS4W0vpCWA+7kmH4/IiLSSajggYhIm1PwU8eAzER6psZxoKiSGX/9lJvsh/mlE9j5kXWLUKUjlbKEXlQk9KYqqTe+lBz8KTkY6f0gNQdHQhpuhw2n3YbLEbzZbTjtBoZhRP8bFBGRjkkFD0RE2pyCnzoMw+D+C4fzjxU72V9YwVtFZ5DlO0KqUYYNExsBDEzsBIJfmxg1HtsIkG6U0MfIJ90oJc5XRFxxERRvbPD1Ssx4KnHhwUmZ6aQKBx6cVOHEhxOfzYnPcOEzXARsTvw2NwG7i4DNheGIw+5y43DF4XDH43LF4YqLxx2XQHx8Ag5XHKbdhWF3YzpcmDYXhsONaXeBw0WV6aDYa6OwyqDYE6C4wkdxpZfiCi/FlT48Xh9xDhvxToM4p8167DCIcxg47HY25YJ37QES3E7cwcDN7bCHA7h4l52sFDcJrpb9mvkDJl5/AH/AxBcw8dV4XPMeTEwTTAjeW18DpMY7yU6Jw2aLfiBpmiYBE+xtcG4R6Xo07E1EpO3FNPh5/PHH+cMf/kBubi6jRo3iscceY9y4cbFsEgBThmUxZVgWYF3gFpZfwr7CCg4UVbK/sIL9RRUcLq3CwLrwtdkMbAbYDeuxPZixMT3FxJXtJ6liH0mVB0iryiXDm0t3Xx49AnmkUUKyUUEywflEjV1Dm8FboO2+Z79pYGJg/Qs2wzzqMT7TRsV+N5W4qDBdVOCmAhcVZhxHcFGBi9W4CdjjSHA5SHTbSXDZSQze4p0GVX6TyioflV4flVV+Kr1+yr0BSrwGpV4DL3Z82KnCgc+048OBFzteHPiw48XeYNtqdqXDZtAt0UW3JBfdEt1kJrnolhRHtyQ38W4HHp8fj8+k0mfi8QWo9AXweANU+kzKfVBaFaC0yqSkyqTEY1JcFaDYY1Ls8VMVsJGSEEe35HgykxPITImjR2oC3ZPjyUpLpFtyHKZhwxcw8JkG3gB4A4YV2Hkr8VVWUH54NzvWfoTLFsBu+rCbVdj8XmymF1vAi8dTSYXPoMxvo9RrUOqzUeI1KPHaKK4CDw58hhO/4SRgOAgYdkxsYLMBNpISXPROTyQnI5HeGYn0Tk8kzumsMVTTqO4xw8AEyqoCHCqt4lCJh1KPF5thYLcZ2A2wE8Bp+LGbPhz4sQX8eH1V+Kqq8Pmq8Hmr8Hm9+PxevAEDj+nAZndid7iwOZw4nC5sDhd2pxOHw4XT5SYjyU33ZDfJbkeHy3hW+aygO85p63Btk2OIaarggYhIO4hZ8PPKK68wa9Ys5s+fz/jx45k3bx7Tpk1j8+bN9OjRcf7jNwyD9EQX6YkuhvdOje7JPaVQcgB8leCrwu+txF9VgbeqEl9VJX6vB7+nAp+3koDXg7+qgoDPg+n1EPB5CFRV4PN6CHgrMb2VmL4q8Hsw/B5s/iqcZhUOfDhNL65guODChxMvbry1mmI3QhFW8zmMAMkEg7ejXRNWBW8RvUCE+zelMnjLr/9UfEvPGWqfDygI3lpgGMDuljYi+gwgKXgbENwWMK2gyN6MoLglfKYNP3YqMTDrzKkzjOqcqx+DAIbVHjOY8TNNTjUDFHz5MwzDCt5r/jpWxytG8LFRHfAZ1vBSEwO/CQHTxG8aBEzwBaxt/uD3bho2bIaBzWbHZrNhs9mw22w4bFYm2DD9EPBb96Yfw7S22YKPQ1njcK7YMMK5Y9MI3mOzHhu26vYFv4dQAOqwGdhtNisYDX7wAlgXz5hgBqysZCCAaQYwA9bXmAFcdgPMgLXv1N/C6Cva5OcpLeApwfBVWo+V+RERaTMxC37mzp3LjTfeyPXXXw/A/PnzWbBgAc888wx33nlnrJrVvtxJ4B4c/tIevLna47VNEwI+q4S3v6q6lHfoorPOBWKtxxh4qzy8t3gBZ31rAk7Ta62D5C2vd19ZUUJpSQkllT5KKr2UVPoprvRR7PFR5vHjdtpJdDuCNydJbidJboMkh0mi08RJIJwBMQI+jIAP/F6rzaHHjX4abxAwTSp9ASqqfJRX+amo8lNR5aPC66Oiyo9pmlbWzmbgsIHTZmC3WV87DXDaArhsVqDnspk4jAAOrHu7EcAwA/h9Xvw+H/6An4DfTyDgxwz4MQPWBa8tPCzSurcTAINgOOrAY1qZGy+O8K0KB1Wm9di0OXDZTOJsAeIMHy6bHzd+nIYPZyhTFLAyRfaAD5vpA0zr4jvCgLYxR8sEhjJxfsOBP3hvGjbsBHCYXuym38rhmT7r+6/D6tca2yONxcMBQAPPtaYL6kZSYGVgo5GFbZs4ErCa3HBOtAZVsOxYglkfny0OnAkxboyIyLErJsFPVVUVq1atYvbs2eFtNpuNyZMns3Llynr7ezwePJ7qdXaKi4sB8Hq9eL3eevsfTeiYlhx7zLG5rZszsqp0XoeDSmc63uQccDob3c8OpAZvLWUC/lYc7wzeUups9wfnEbkcLR/OZGLVi7cFX6PWc6ZJhdeP3TBw2G315ga5AMPrZdmSJUyZMgVnE/0YSXtq9VWNbABmADPg50iZh31Hyth7pJx9BWVU+f10T3LTLdFJZqKTjEQXmYkO4p2O4LFm9b3NDjaHdbM7wea0tmH9Z3K0/1ACQMAMVAeu4XsvlR4vBeWVHCnxUFBWyZEyD4VlHorKPTgNkwSXjQSHQbwzeHM5iHfacNoMNqxfz7ATh2PYbARMwlmcgIn1td9PlT+AzxfAG/Dj9QXw+f1U+Uz8fj82A5LcDhJcNpJcBokuOwlOG4luO4lOGwYm5R4vpR4v5R4fpZVeyjxea1uVSYUf7HYHDocTp8OB0+nE5XTgcDpxORw4Hc7/3969x0ZRv3sc/2wvu21pS4VCW6AUMAjKaTFA6GmIxqQVWo1UyEEkGAERIwmaiBLjjYKiEk5AIiEh0QASLyAqkPxiFKysFIQatdAYAgFSBKQtl6PuFnrf7/mjsFrbCmW3nd2d9yuZpDszu/vMk2/n6TPf2a2MI7rt7lVjrs0Mmb/NzPhkWlvl87WopdUnn88nX2tr2+faTNtn37wNLfqzvll/Xm3SH/Ut+rO+SU0tbR3UXzNKjmszSG1LTEy04p3RiouNVR9XjP73f3L+ut0xKV0K4PyJILv2D04bYpLlsjgUAIhkljQ/ly5dUmtrq9LS0tqtT0tL07Fjxzrs//bbb2v58uUd1u/evVsJCbd+hWzPnj23/Fy0IYfBYVUeB1//wSM1eqTf1LaEghhJqVFSaqLUNpdxrQlrkRpapIb6Fv+dhn0GDNOvF+pu+rWjJDkVJWeMpJhrcyRGqm+U6huNLqrlBpHFSIqXXFKiq+0WwX/yNfvU1NyoJnX3HyRfm7dxRCsmuu2dXDFt75Pxt6sIxkiNrZKnWfI2S1EOyRUtxV1bXNFS9D96+i9/Pvu3R1XdjKvN1atXb+l5uIH0/1LL4//Rke/3y/pPvgJA5AqLb3t76aWXtHjxYv9jj8ejzMxMTZ48WcnJ/7yef2PNzc3aE8Sr7XZEDoODPAaOHPau6zPvCDJXkkzmf+tS0v9ZHQkARDRLmp/U1FRFR0ertra23fra2lqlp6d32N/lcsnl6ngjQGxsbEB/7AT6fJDDYCGPgSOHvYMcAwDCWZQVb+p0OjV+/HiVlpb61/l8PpWWliovL8+KkAAAAABEOMtue1u8eLHmzJmjCRMmaOLEiVq7dq2uXLni//Y3AAAAAAgmy5qfmTNn6uLFi1q6dKlqamp0991366uvvurwJQgAAAAAEAyWfuHBokWLtGjRIitDAAAAAGATlnzmBwAAAAB6G80PAAAAAFug+QEAAABgCzQ/AAAAAGyB5gcAAACALdD8AAAAALAFmh8AAAAAtkDzAwAAAMAWaH4AAAAA2EKM1QHcCmOMJMnj8dzS85ubm3X16lV5PB7FxsYGMzTbIIfBQR4DRw571/Xz7vXzMP5CbbIeOQwcOQwO8ti7ulObwrL58Xq9kqTMzEyLIwEAe/J6verbt6/VYYQUahMAWOtmapPDhOHlO5/Pp/PnzyspKUkOh6Pbz/d4PMrMzNTZs2eVnJzcAxFGPnIYHOQxcOSwdxlj5PV6NWjQIEVFcef031GbrEcOA0cOg4M89q7u1KawnPmJiorSkCFDAn6d5ORkBmSAyGFwkMfAkcPew4xP56hNoYMcBo4cBgd57D03W5u4bAcAAADAFmh+AAAAANiCLZsfl8ulkpISuVwuq0MJW+QwOMhj4MghIgVjOXDkMHDkMDjIY+gKyy88AAAAAIDusuXMDwAAAAD7ofkBAAAAYAs0PwAAAABswfbNj8Ph0M6dO60OI2yRv+A7ffq0HA6HDh8+bHUoYYscIpxxXg0cOQw+zquBI4ehwRbNz/r16zVs2DDFxcUpNzdXP/zwg9UhhY1ly5bJ4XC0W0aPHm11WCFt3759euihhzRo0KBOC7AxRkuXLlVGRobi4+NVUFCgEydOWBNsiLpRDufOndthXBYWFloTLHALqEuBoTZ1H7UpcNSmyBDxzc+2bdu0ePFilZSU6Oeff9bYsWM1ZcoUXbhwwerQwsaYMWNUXV3tX/bv3291SCHtypUrGjt2rNavX9/p9lWrVundd9/Vhg0bVF5erj59+mjKlClqaGjo5UhD141yKEmFhYXtxuUnn3zSixECt466FBzUpu6hNgWO2hQZIr75WbNmjRYsWKB58+bprrvu0oYNG5SQkKCNGzd2un9JSYkyMjJUWVnZy5GGrpiYGKWnp/uX1NTULvclf1JRUZFWrFihadOmddhmjNHatWv16quvqri4WDk5OdqyZYvOnz/f5S0ara2teuKJJzR69GidOXOmh6MPDf+Ww+tcLle7cXnbbbd1ua8dc4jQRV0KDmpT91CbAkdtigwR3fw0NTXpp59+UkFBgX9dVFSUCgoKdPDgwXb7GmP0zDPPaMuWLSorK1NOTk5vhxuyTpw4oUGDBmnEiBGaPXt2p7+g5O/mVFVVqaampt2Y7Nu3r3JzczuMSUlqbGzUjBkzdPjwYZWVlWno0KG9GW5Ic7vdGjhwoEaNGqWFCxfq8uXLne5HDhFKqEvBQ20KHmpT8FCbQl+M1QH0pEuXLqm1tVVpaWnt1qelpenYsWP+xy0tLXrsscdUUVGh/fv3a/Dgwb0dasjKzc3V5s2bNWrUKFVXV2v58uW655579MsvvygpKUkS+euOmpoaSep0TF7fdl1dXZ0efPBBNTY2au/everbt2+vxRnqCgsLNX36dA0fPlynTp3Syy+/rKKiIh08eFDR0dH+/cghQg11KTioTcFFbQoOalN4iOjm52Y999xzcrlcOnTo0L9Om9tRUVGR/+ecnBzl5uYqKytLn376qebPny+J/PWUWbNmaciQIfr2228VHx9vdTgh5dFHH/X/nJ2drZycHN1+++1yu93Kz8/3byOHCFecV/8dtck6nFe7Rm0KDxF921tqaqqio6NVW1vbbn1tba3S09P9j++//3799ttv+vrrr3s7xLCTkpKiO+64QydPnvSvI3837/q4u9GYlKQHHnhAlZWVnd5ygPZGjBih1NTUduNSIocIPdSlnkFtCgy1qWdQm0JTRDc/TqdT48ePV2lpqX+dz+dTaWmp8vLy/OumTp2qjz/+WE8++aS2bt1qRahho66uTqdOnVJGRoZ/Hfm7ecOHD1d6enq7MenxeFReXt5uTErSwoULtXLlSk2dOlXfffddb4caVs6dO6fLly+3G5cSOUTooS71DGpTYKhNPYPaFKJMhNu6datxuVxm8+bN5ujRo+app54yKSkppqamxhhjjCSzY8cOY4wx27dvN3FxcWb79u0WRhxann/+eeN2u01VVZU5cOCAKSgoMKmpqebChQvGGPLXGa/XayoqKkxFRYWRZNasWWMqKirMr7/+aowxZuXKlSYlJcXs2rXLVFZWmuLiYjN8+HBTX19vjDGmqqrKSDIVFRXGGGPeeecdk5iYaMrKyqw6pF73bzn0er3mhRdeMAcPHjRVVVXmm2++MePGjTMjR440DQ0NxhhyiNBGXQoctan7qE2BozZFhohvfowxZt26dWbo0KHG6XSaiRMnmkOHDvm3/f0EaYwx27ZtM3Fxcebzzz+3INLQM3PmTJORkWGcTqcZPHiwmTlzpjl58qR/O/nraO/evUZSh2XOnDnGGGN8Pp957bXXTFpamnG5XCY/P98cP37c//x/nhyNMWb16tUmKSnJHDhwoJePxhr/lsOrV6+ayZMnmwEDBpjY2FiTlZVlFixY4P/D0RhyiNBHXQoMtan7qE2BozZFBocxxvTcvBIAAAAAhIaI/swPAAAAAFxH8wMAAADAFmh+AAAAANgCzQ8AAAAAW6D5AQAAAGALND8AAAAAbIHmBwAAAIAt0PwAAAAAsAWaHwAAAAC2QPMD9JC5c+fq4YcftjoMAAD8qE2wO5ofAAAAALZA8wME6LPPPlN2drbi4+PVv39/FRQUaMmSJfrggw+0a9cuORwOORwOud1uSdLZs2f1yCOPKCUlRf369VNxcbFOnz7tf73rV+WWL1+uAQMGKDk5WU8//bSampqsOUAAQNihNgGdi7E6ACCcVVdXa9asWVq1apWmTZsmr9ersrIyPf744zpz5ow8Ho82bdokSerXr5+am5s1ZcoU5eXlqaysTDExMVqxYoUKCwtVWVkpp9MpSSotLVVcXJzcbrdOnz6tefPmqX///nrzzTetPFwAQBigNgFdo/kBAlBdXa2WlhZNnz5dWVlZkqTs7GxJUnx8vBobG5Wenu7f/8MPP5TP59P7778vh8MhSdq0aZNSUlLkdrs1efJkSZLT6dTGjRuVkJCgMWPG6PXXX9eSJUv0xhtvKCqKCVsAQNeoTUDXGKlAAMaOHav8/HxlZ2drxowZeu+99/T77793uf+RI0d08uRJJSUlKTExUYmJierXr58aGhp06tSpdq+bkJDgf5yXl6e6ujqdPXu2R48HABD+qE1A15j5AQIQHR2tPXv26Pvvv9fu3bu1bt06vfLKKyovL+90/7q6Oo0fP14fffRRh20DBgzo6XABADZAbQK6RvMDBMjhcGjSpEmaNGmSli5dqqysLO3YsUNOp1Otra3t9h03bpy2bdumgQMHKjk5ucvXPHLkiOrr6xUfHy9JOnTokBITE5WZmdmjxwIAiAzUJqBz3PYGBKC8vFxvvfWWfvzxR505c0ZffPGFLl68qDvvvFPDhg1TZWWljh8/rkuXLqm5uVmzZ89WamqqiouLVVZWpqqqKrndbj377LM6d+6c/3Wbmpo0f/58HT16VF9++aVKSkq0aNEi7qkGANwQtQnoGjM/QACSk5O1b98+rV27Vh6PR1lZWVq9erWKioo0YcIEud1uTZgwQXV1ddq7d6/uu+8+7du3Ty+++KKmT58ur9erwYMHKz8/v93Vtvz8fI0cOVL33nuvGhsbNWvWLC1btsy6AwUAhA1qE9A1hzHGWB0EgL/MnTtXf/zxh3bu3Gl1KAAASKI2IXIwTwkAAADAFmh+AAAAANgCt70BAAAAsAVmfgAAAADYAs0PAAAAAFug+QEAAABgCzQ/AAAAAGyB5gcAAACALdD8AAAAALAFmh8AAAAAtkDzAwAAAMAW/h9o/djYsFFRzQAAAABJRU5ErkJggg==",
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def plot_record_curves(record_dict, sample_step=500):\n",
    "    # .set_index(\"step\") 将 step 列设置为 DataFrame 的索引\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    last_step = train_df.index[-1]  # 最后一步的步数\n",
    "\n",
    "    # print(train_df)\n",
    "    # print(val_df)\n",
    "\n",
    "    # 画图 \n",
    "    fig_num = len(train_df.columns)  # 画两张图,分别是损失和准确率\n",
    "\n",
    "    # plt.subplots：用于创建一个包含多个子图的图形窗口。\n",
    "    # 1：表示子图的行数为 1。\n",
    "    # fig_num：表示子图的列数，即子图的数量。\n",
    "    # figsize=(5 * fig_num, 5)：设置整个图形窗口的大小，宽度为 5 * fig_num，高度为 5。\n",
    "    # fig：返回的图形对象（Figure），用于操作整个图形窗口。\n",
    "    # axs：返回的子图对象（Axes 或 Axes 数组），用于操作每个子图。\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        # train_df.index 是 x 轴数据（通常是 step）。\n",
    "        # train_df[item] 是 y 轴数据（当前指标的值）。\n",
    "        axs[idx].plot(train_df.index, train_df[item], label=\"train:\" + item)\n",
    "        # val_df.index 是 x 轴数据。\n",
    "        # val_df[item] 是 y 轴数据。\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=\"val:\" + item)\n",
    "        axs[idx].grid()  # 显示网格\n",
    "        axs[idx].legend()  # 显示图例\n",
    "        axs[idx].set_xticks(range(0, train_df.index[-1] + 1, 5000))  # 设置x轴刻度\n",
    "        axs[idx].set_xticklabels(map(lambda x: f\"{x // 1000}k\", range(0, last_step + 1, 5000)))  # 设置x轴标签\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "plot_record_curves(record_dict)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d9729d15a0d01e8",
   "metadata": {},
   "source": [
    "## 评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "3195e10a60c29806",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-21T14:10:19.121498Z",
     "start_time": "2025-01-21T14:10:19.121498Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test loss: 0.4513, Test acc: 0.8518\n"
     ]
    }
   ],
   "source": [
    "# 加载最好的模型\n",
    "# torch.load：加载保存的模型权重或整个模型。\n",
    "# \"checkpoints/best.ckpt\"：模型权重文件路径。\n",
    "# weights_only=True：仅加载模型的权重，而不是整个模型（包括结构和参数）。这是 PyTorch 2.1 引入的新特性，用于增强安全性。\n",
    "# map_location=device：将模型加载到当前设备（GPU或CPU）。\n",
    "model.load_state_dict(torch.load(\"checkpoints/08_resnet.ckpt\", weights_only=True, map_location=device))  # 加载最好的模型\n",
    "\n",
    "model.eval()  # 评估模式\n",
    "loss, acc = evaluate(model, eval_loader, loss_fct)\n",
    "print(f\"Test loss: {loss:.4f}, Test acc: {acc:.4f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
